从文件夹中删除文件的单元测试方法

时间:2018-06-19 13:12:52

标签: c# unit-testing moq

我们有一个以文件夹名称和天数为参数的方法。

    public void Delete(string folder, int days)
    {
        var files = Directory.GetFiles(folder);

        foreach (var file in files)
        {
            var fi = new FileInfo(file);
            var fiCreationTime = fi.CreationTime;
            var deleteOlderThan= DateTime.Now.AddDays(-days);

            if (fiCreationTime >= deleteOlderThan) continue;
                fi.Delete();
        }
    }

在c#中对此类方法进行单元测试的最佳方法是什么

5 个答案:

答案 0 :(得分:2)

实际上,您不能对方法进行单元测试,因为它取决于外部API( FileSystem,DateTime )。

因此,您应该做的是分离逻辑并与外部源集成,它看起来像这样:

public class MyFileInfo
{
    public string FileName { get; set; }

    public DateTime CreationTime { get; set; }
}

public interface IDateTimeProvider
{
    DateTime GetCurrentTime();
}

public interface IMyFileSystemService
{
    IEnumerable<MyFileInfo> GetFileInfos(string folder);

    void DeleteFile(MyFileInfo myFileInfo);
}

public class MyService
{
    private readonly IMyFileSystemService _myFileSystemService;
    private readonly IDateTimeProvider _dateTimeProvider;

    public MyService(IMyFileSystemService myFileSystemService, IDateTimeProvider dateTimeProvider)
    {
        _myFileSystemService = myFileSystemService;
        _dateTimeProvider = dateTimeProvider;
    }

    public void Delete(string folder, int days)
    {
        var files = _myFileSystemService.GetFileInfos(folder);

        foreach (var file in files)
        {
            var deleteOlderThan = _dateTimeProvider.GetCurrentTime().AddDays(-days);

            if (file.CreationTime >= deleteOlderThan) continue;
            _myFileSystemService.DeleteFile(file);
        }
    }
}

我认为接口IDateTimeProviderIMyFileSystemService的实现应该不是问题。

现在您可以为MyService.Delete

编写干净的单元测试了

答案 1 :(得分:1)

100%的解决方案是必须注入很多,因为它具有副作用或不确定性:

1) Directory.GetFiles
2) new FileInfo(file)
3) fi.CreationTime
4) DateTime.Now.AddDays
5) fi.Delete

例如,您注入在生产中返回日期时间的datetime服务,在测试中总是返回某个固定日期的datetime服务。并使用模拟框架来检查有时是否调用了delete方法,而在其他时候未调用它。

答案 2 :(得分:1)

如果您要对已显示的方法进行单元测试,那么当前编写方法将很困难,但是我们可以模拟File类并传递一个接口供其使用:

public class FileDeleter
{

    private readonly IFileOperator _fileOperator;

    public FileDeleter(IFileOperator fileOperator)
    {
        _fileOperator= fileOperator
    }

    public void Delete(string folder, int days)
    {
        var files = _fileClass.GetFiles(folder);

        foreach (var file in files)
        {
            var fi = _fileClass.GetFileInfo(file);
            var fiCreationTime = fi.CreationTime;
            var deleteOlderThan= DateTime.Now.AddDays(-days);

            if (fiCreationTime >= deleteOlderThan)
                continue;
            fi.Delete();
        }
    }
}

public interface IFileClass 
{
    IEnumerable<string> GetFiles(string path);
    IFileInfo GetFileInfo(string filePath);
}

public interface IFileInfo 
{
    DateTime CreationTime { get; }
    void Delete();
}

之后,只需使用https://github.com/Moq/moq4/wiki/Quickstart

这样的库来模拟两个类

并编写单元测试以测试所需的任何逻辑。

编辑:正如其他人所指出的那样,datetime.now可能也是一件好事,但可以用相同的方式来完成。

答案 3 :(得分:1)

另一种可能性是使用静态帮助器类

public static class FileEx
{
    public static Func<string, IEnumerable<string>> EnumerateFiles { set; get; }
       = Directory.EnumerateFiles;
}

,然后仅使用帮助程序类:

var files = FileEx.EnumerateFiles(...);

这样,您可以在单元测试中更改方法。

[Test]
public void Test()
{
    FileEx.EnumerateFiles = (_) => new [] { "file1", "file2" };

    // your test here

    // Reset the method:
    FileEx.EnumerateFiles = Directory.EnumerateFiles;
}

这适用于大多数静态辅助方法,并且重构每个类都更加容易,因此可以将其注入。

缺点

  • 您将失去函数重载。
  • 仅适用于静态类(在您的示例中,它不适用于FileInfo)。

优势

  • 真的很容易
  • 易于实施
  • 测试时易于更改
  • 易于使用

更新为评论中的评论:

在单元测试中将系统方法替换为Directory.EnumerateFiles是可行的。 由于您正在测试Delete方法,因此可以假定Microsoft已经测试了框架代码。因此,单元测试唯一必须证明的是Delete方法必须纠正输出和副作用。

答案 4 :(得分:0)

您的代码的理想解决方案是创建一个接口,该接口包含用于所有文件操作的方法,然后模拟这些方法

但是您还可以在示例类中为这些文件操作创建虚拟方法,并在单元测试中模拟该方法

以下是您实际代码的代码实现

public class Sample
{
    public void Delete(string folder, int days)
    {
        var files = GetFiles(folder);

        foreach (var file in files)
        {
            var fi = GetFileInfo(file);
            var fiCreationTime = fi.CreationTime;
            var deleteOlderThan = DateTime.Now.AddDays(-days);

            if (fiCreationTime >= deleteOlderThan) continue;
            DeleteFile(fi);
        }
    }

    public virtual void DeleteFile(FileInfo f)
    {
        f.Delete();
    }

    public virtual string[] GetFiles(string path)
    {
        return Directory.GetFiles(path);
    }

    public virtual FileInfo GetFileInfo(string file)
    {
        return new FileInfo(file);
    }
}

以下是您的单元测试课程

public class NUnitTest
{
    [TestFixture]
    public class UnitTest1
    {
        private Mock<Sample> _sample;
        private FileInfo _fileInfo;
        [SetUp]
        public void Setup()
        {
            _sample = new Mock<Sample>();

        }

        [Test]
        public void File_Should_Not_Delete()
        {
            _fileInfo = new FileInfo("file");
            _fileInfo.Create();

            _sample.Setup(x => x.GetFiles(It.IsAny<string>())).Returns(() => new[] {"file1"});
            _sample.Setup(x => x.GetFileInfo(It.IsAny<string>())).Returns(() => _fileInfo);
            _sample.Setup(x => x.DeleteFile(It.IsAny<FileInfo>())).Verifiable();
            _sample.Object.Delete("file1",2);

            _sample.Verify(x => x.DeleteFile(It.IsAny<FileInfo>()), Times.Never);

        }

        [Test]
        public void File_Should_Delete()
        {
            _fileInfo = new FileInfo("file1");
            _fileInfo.Create();

            _sample.Setup(x => x.GetFiles(It.IsAny<string>())).Returns(() => new[] { "file1" });
            _sample.Setup(x => x.GetFileInfo(It.IsAny<string>())).Returns(() => _fileInfo);
            _sample.Setup(x => x.DeleteFile(It.IsAny<FileInfo>())).Verifiable();
            _sample.Object.Delete("file1", -2);

            _sample.Verify(x => x.DeleteFile(It.IsAny<FileInfo>()), Times.Once);

        }
    }
}

我知道这不是一个好的设计实践,但我只是想向您展示一种使用虚拟方法进行单元测试的不同方法。

最好的方法是创建一个接口,并模拟我作为虚拟接口创建的那些接口方法