C# 从文件夹中删除文件的单元测试方法
我们有一个以文件夹名称和天数为参数的方法C# 从文件夹中删除文件的单元测试方法,c#,unit-testing,moq,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.Cre
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#中对此类方法进行单元测试的最佳方法是什么?100%的解决方案是,必须注入大量此类方法,因为它要么有副作用,要么是非决定性的:
1) Directory.GetFiles
2) new FileInfo(file)
3) fi.CreationTime
4) DateTime.Now.AddDays
5) fi.Delete
例如,在生产环境中注入datetimeservice返回datetime,在测试环境中注入datetimeservice总是返回某个固定日期。并使用模拟框架检查是否有时调用delete方法,有时不调用它。实际上,您无法对您的方法进行单元测试,因为它依赖于外部API(文件系统、日期时间) 因此,您应该做的是分离逻辑并与外部源集成,它可能如下所示:
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);
}
}
}
公共类MyFileInfo
{
公共字符串文件名{get;set;}
公共日期时间CreationTime{get;set;}
}
公共接口IDateTimeProvider
{
DateTime GetCurrentTime();
}
公共接口IMyFileSystemService
{
IEnumerable GetFileInfos(字符串文件夹);
作废删除文件(MyFileInfo MyFileInfo);
}
公共类MyService
{
私有只读IMyFileSystemService\u myFileSystemService;
私有只读IDateTimeProvider\u dateTimeProvider;
公共MyService(IMyFileSystemService myFileSystemService,IDateTimeProvider dateTimeProvider)
{
_myFileSystemService=myFileSystemService;
_dateTimeProvider=dateTimeProvider;
}
公共作废删除(字符串文件夹,整数天)
{
var files=\u myFileSystemService.GetFileInfos(文件夹);
foreach(文件中的var文件)
{
var deleteOlderThan=\u dateTimeProvider.GetCurrentTime().AddDays(-days);
如果(file.CreationTime>=deleteOlderThan)继续;
_myFileSystemService.DeleteFile(文件);
}
}
}
我认为接口IDateTimeProvider
和IMyFileSystemService
的实现不应该成为问题
现在您可以为
MyService编写干净的单元测试了。删除如果您想要单元测试您已经展示过的方法,那么当前编写的方法将很困难,但是我们可以模拟文件
类,并传递一个接口供其使用:
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();
}
公共类文件删除程序
{
专用只读IFileOperator\u fileOperator;
公共文件删除程序(IFileOperator文件操作员)
{
_fileOperator=fileOperator
}
公共作废删除(字符串文件夹,整数天)
{
var files=\u fileClass.GetFiles(文件夹);
foreach(文件中的var文件)
{
var fi=\u fileClass.GetFileInfo(文件);
var fiCreationTime=fi.CreationTime;
var deleteOlderThan=DateTime.Now.AddDays(-days);
如果(fiCreationTime>=deleteOlderThan)
继续;
fi.删除();
}
}
}
公共接口IFileClass
{
IEnumerable GetFiles(字符串路径);
IFileInfo GetFileInfo(字符串文件路径);
}
公共接口IFileInfo
{
DateTime CreationTime{get;}
作废删除();
}
之后,只需使用如下库模拟这两个类:
编写单元测试来测试所需的逻辑
编辑:正如其他人所指出的,datetime.now可能也是一件值得模仿的好事情,但也可以用同样的方法来完成。另一种可能性是使用静态帮助器类
public static class FileEx
{
public static Func<string, IEnumerable<string>> EnumerateFiles { set; get; }
= Directory.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
方法必须纠正输出和副作用。对于您的代码来说,完美的解决方案是创建一个包含所有文件操作方法的接口,然后模拟这些方法
但您也可以在示例类中为这些文件操作创建虚拟方法,并在单元测试中模拟该方法
下面是实际代码的代码实现
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);
}
}
}
公共类NUnitTest
{
[测试夹具]
公共类UnitTest1
{
私人模拟样本;
私人文件信息(FileInfo);;
[设置]
公共作废设置()
{
_样本=新模拟();
}
[测试]
公共无效文件\u不应\u删除()
{
_fileInfo=新的fileInfo(“文件”);
_Create();
_Setup(x=>x.GetFiles(It.IsAny())。返回(()=>new[]{“file1”});
_sample.Setup(x=>x.GetFileInfo(It.IsAny())。返回(()=>\u fileInfo);
_sample.Setup(x=>x.DeleteFile(It.IsAny()).Verifiable();
_sample.Object.Delete(“file1”,2);
_Verify(x=>x.DeleteFile(It.IsAny()),Times.Never);
}
[测试]
公共无效文件\u应\u删除()
{
_fileInfo=新的fileInfo(“file1”);
_Create();
_Setup(x=>x.GetFiles(It.IsAny())。返回(()=>new[]{“file1”});
_sample.Setup(x=>x.GetFileInfo(It.IsAny())。返回(()=>\u fileInfo);
_sample.Setup(x=>x.DeleteFile(It.IsAny()).Verifiable();
_sample.Object.Delete(“file1”,-2);
_验证(x=>x.DeleteFile(It.IsAny()),次.Once);
}
}
}
我知道这不是一个好的设计实践,但我
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);
}
}
}