我们有一个以文件夹名称和天数为参数的方法。
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#中对此类方法进行单元测试的最佳方法是什么
答案 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);
}
}
}
我认为接口IDateTimeProvider
和IMyFileSystemService
的实现应该不是问题。
现在您可以为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;
}
这适用于大多数静态辅助方法,并且重构每个类都更加容易,因此可以将其注入。
在单元测试中将系统方法替换为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);
}
}
}
我知道这不是一个好的设计实践,但我只是想向您展示一种使用虚拟方法进行单元测试的不同方法。
最好的方法是创建一个接口,并模拟我作为虚拟接口创建的那些接口方法