单元测试文件 I/O

通过阅读 Stack Overflow 上现有的单元测试相关线程,我找不到一个关于如何进行单元测试文件 I/O 操作的清晰答案。我最近才开始研究单元测试,之前我已经意识到了单元测试的优点,但是很难习惯先编写测试。我已经将我的项目设置为使用 NUnit 和 Rhino Mocks,尽管我理解了它们背后的概念,但是我在理解如何使用 Mock Object 方面遇到了一些麻烦。

具体来说,我有两个问题想要回答。首先,单元测试文件 I/O 操作的正确方法是什么?其次,在我尝试学习单元测试的过程中,我遇到了一些依赖注入。在设置并运行 Nregister 之后,我想知道是否应该在单元测试中使用 DI,或者只是直接实例化对象。

57859 次浏览

使用 犀牛模仿者系统包装器检查 TDD 教程

SystemWrapper 包装了许多 System.IO 类,包括 File、 FileInfo、 Directory、 DirectoryInfo 等。

在本教程中,我将展示如何使用 MbUnit 进行测试,但是对于 NUnit 来说这是完全相同的。

你的测试会是这样的:

[Test]
public void When_try_to_create_directory_that_already_exists_return_false()
{
var directoryInfoStub = MockRepository.GenerateStub<IDirectoryInfoWrap>();
directoryInfoStub.Stub(x => x.Exists).Return(true);
Assert.AreEqual(false, new DirectoryInfoSample().TryToCreateDirectory(directoryInfoStub));


directoryInfoStub.AssertWasNotCalled(x => x.Create());
}

问题1:

你有三个选择。

选项1: 忍受它。

(没有例子: P)

选项2: 在需要的地方创建一个轻微的抽象。

代替执行文件 I/O (File。ReadAllBytes 或其他) ,您可以更改它,以便 IO 在外部完成,而是传递流。

public class MyClassThatOpensFiles
{
public bool IsDataValid(string filename)
{
var filebytes = File.ReadAllBytes(filename);
DoSomethingWithFile(fileBytes);
}
}

会变成

// File IO is done outside prior to this call, so in the level
// above the caller would open a file and pass in the stream
public class MyClassThatNoLongerOpensFiles
{
public bool IsDataValid(Stream stream) // or byte[]
{
DoSomethingWithStreamInstead(stream); // can be a memorystream in tests
}
}

这种方法是一种权衡。首先,是的,它更容易测试。然而,它用可测试性换取了稍微增加的复杂性。这可能会影响到可维护性和必须编写的代码量,而且您可能只是将测试问题提升了一个级别。

然而,根据我的经验,这是一种很好的、平衡的方法,因为您可以概括并使重要的逻辑可测试,而不需要将自己投入到一个完全包装的文件系统中。也就是说,你可以归纳出你真正关心的部分,而其余部分保持不变。

选项3: 包装整个文件系统

更进一步,模仿文件系统可能是一种有效的方法; 这取决于您愿意忍受多大的膨胀。

我以前走过这条路线; 我有一个包装好的文件系统实现,但最后我只是删除了它。API 有细微的差别,我不得不到处注入它,最终它是额外的痛苦,因为使用它的许多类对我来说并不是非常重要。如果我一直使用 IoC 容器或者写一些关键的东西,并且测试需要快速,我可能会坚持下去。正如所有这些选项一样,你的里程数可能会有所不同。

至于你们的集装箱问题:

手动注入测试双精度。如果需要进行大量的重复性工作,那么只需在测试中使用设置/工厂方法。使用 IoC 容器进行测试将是极端的过度杀伤!也许我没有理解你的第二个问题。

目前,我通过依赖注入使用一个 ifileSystem 对象。对于生产代码,包装类实现接口,包装我需要的特定 IO 函数。在测试时,我可以创建一个 null 或 stub 实现,并将其提供给被测试的类。被测试的班级一点也不聪明。

在测试文件系统时,不一定要执行 操作。事实上,根据具体情况,你可能会做几件事。

你需要问的问题是: 我在测试什么?

  • 文件系统有用吗?你可能不需要测试 那个,除非你使用的是一个你非常不熟悉的操作系统。例如,如果您只是简单地给出一个保存文件的命令,那么编写一个测试来确保文件真正得到保存就是在浪费时间。

  • 把文件保存到正确的地方?那么,你怎么知道什么是正确的地方呢?假设您有将路径与文件名组合在一起的代码。这是您可以轻松测试的代码: 您的输入是两个字符串,并且您的输出应该是一个字符串,该字符串是使用这两个字符串构造的有效文件位置。

  • 从目录中获取正确的文件集?您可能需要为 file-getter 类编写一个测试来真正测试文件系统。但是您应该使用一个测试目录,其中包含不会更改的文件。您还应该将此测试放在集成测试项目中,因为这不是真正的单元测试,因为它依赖于文件系统。

  • 但是,我得处理一下我拿到的文件。对于 那个测试,应该对 file-getter 类使用 假的。你的赝品应该会返回一个硬编码的文件列表。如果使用 真的文件获取器和 真的文件处理器,就不知道哪一个会导致测试失败。因此,在测试中,您的文件处理器类应该使用一个假的 file-getter 类。您的文件处理器类应该采用 file-getter 接口。在实际代码中,您将传入实际的 file-getter。在测试代码中,您将传递一个假的 file-getter,它返回一个已知的静态列表。

基本原则是:

  • 在不测试文件系统本身时,使用隐藏在接口后面的假文件系统。
  • 如果您需要测试真正的文件操作,那么
    • 将测试标记为集成测试,而不是单元测试。
    • 有一个指定的测试目录、一组文件等,它们总是保持不变的状态,这样您的面向文件的集成测试可以一致地通过。

从2012年开始,您可以使用 微软赝品来实现这一点,而不需要更改代码库,例如,因为它已经被冻结了。

首先是 System.dll 的 生成一个假的装配-或任何其他包,然后模拟预期的返回值,如下所示:

using Microsoft.QualityTools.Testing.Fakes;
...
using (ShimsContext.Create())
{
System.IO.Fakes.ShimFile.ExistsString = (p) => true;
System.IO.Fakes.ShimFile.ReadAllTextString = (p) => "your file content";


//Your methods to test
}

我使用 System.IO.Abstractions NuGet 软件包。

这个网站有一个很好的例子向您展示如何使用注入进行测试。 Http://dontcodetired.com/blog/post/unit-testing-c-file-access-code-with-systemioabstractions

这是从网站复制的代码的副本。

using System.IO;
using System.IO.Abstractions;


namespace ConsoleApp1
{
public class FileProcessorTestable
{
private readonly IFileSystem _fileSystem;


public FileProcessorTestable() : this (new FileSystem()) {}


public FileProcessorTestable(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}


public void ConvertFirstLineToUpper(string inputFilePath)
{
string outputFilePath = Path.ChangeExtension(inputFilePath, ".out.txt");


using (StreamReader inputReader = _fileSystem.File.OpenText(inputFilePath))
using (StreamWriter outputWriter = _fileSystem.File.CreateText(outputFilePath))
{
bool isFirstLine = true;


while (!inputReader.EndOfStream)
{
string line = inputReader.ReadLine();


if (isFirstLine)
{
line = line.ToUpperInvariant();
isFirstLine = false;
}


outputWriter.WriteLine(line);
}
}
}
}
}










using System.IO.Abstractions.TestingHelpers;
using Xunit;


namespace XUnitTestProject1
{
public class FileProcessorTestableShould
{
[Fact]
public void ConvertFirstLine()
{
var mockFileSystem = new MockFileSystem();


var mockInputFile = new MockFileData("line1\nline2\nline3");


mockFileSystem.AddFile(@"C:\temp\in.txt", mockInputFile);


var sut = new FileProcessorTestable(mockFileSystem);
sut.ConvertFirstLineToUpper(@"C:\temp\in.txt");


MockFileData mockOutputFile = mockFileSystem.GetFile(@"C:\temp\in.out.txt");


string[] outputLines = mockOutputFile.TextContents.SplitLines();


Assert.Equal("LINE1", outputLines[0]);
Assert.Equal("line2", outputLines[1]);
Assert.Equal("line3", outputLines[2]);
}
}
}