具有文件系统依赖性的单元测试代码
我正在编写一个组件,给定一个ZIP文件,该组件需要:
- 解压缩文件。
- 在解压缩的文件中找到一个特定的dll。
- 通过反射加载该dll并在其上调用一个方法。
我想对该组件进行单元测试。
我很想编写直接处理文件系统的代码:
void DoIt() { Zip.Unzip(theZipFile, "C:\foo\Unzipped"); System.IO.File myDll = File.Open("C:\foo\Unzipped\SuperSecret.bar"); myDll.InvokeSomeSpecialMethod(); }
但是人们经常说:"不要编写依赖于文件系统,数据库,网络等的单元测试。"
如果我以对单元测试友好的方式编写此代码,我想它看起来应该像这样:
void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner) { string path = zipper.Unzip(theZipFile); IFakeFile file = fileSystem.Open(path); runner.Run(file); }
耶!现在可以测试了;我可以将测试双打(模拟)输入DoIt方法。但是要花多少钱呢?我现在必须定义3个新接口才能使其可测试。而我到底在测试什么呢?我正在测试我的DoIt函数可以正确地与其依赖项进行交互。它无法测试zip文件是否已正确解压缩,等等。
感觉好像我不再在测试功能。感觉就像我只是在测试类的交互。
我的问题是:对依赖于文件系统的内容进行单元测试的正确方法是什么?
编辑我正在使用.NET,但是该概念也可以应用Java或者本机代码。
解决方案
一种方法是编写解压缩方法以获取InputStreams。然后,单元测试可以使用ByteArrayInputStream从字节数组构造这样的InputStream。该字节数组的内容在单元测试代码中可以是一个常量。
假设"文件系统交互"已在框架本身中进行了很好的测试,请创建用于流的方法并进行测试。由于框架创建者已经对FileStream.Open进行了良好的测试,因此可以将打开FileStream并将其传递给该方法的测试忽略掉。
我不愿使用仅用于促进单元测试的类型和概念来污染我的代码。当然,如果它可以使设计更干净,更好,那就更好了,但是我认为通常并非如此。
我对此的看法是,单元测试将尽其所能,这可能不是100%的覆盖率。实际上,可能只有10%。关键是,单元测试应该快速并且没有外部依赖性。他们可能会测试类似"这种情况,当我们为此参数传入null时,此方法将引发ArgumentNullException"。
然后,我将添加可以具有外部依赖性并测试诸如此类的端到端方案的集成测试(也是自动化的,并且可能使用相同的单元测试框架)。
在测量代码覆盖率时,我同时测量单元测试和集成测试。
我们不应该测试类的交互和函数调用。相反,我们应该考虑进行集成测试。测试所需的结果,而不是文件加载操作。
命中文件系统没有任何问题,只需将其视为集成测试而不是单元测试。我将硬编码路径与相对路径交换,并创建一个TestData子文件夹以包含单元测试的zip。
如果集成测试花费的时间太长,请把它们分开,这样它们就不会像快速单元测试那样频繁地运行。
我同意,有时我认为基于交互的测试可能会导致过多的耦合,并且常常最终无法提供足够的价值。我们真的想在这里测试将文件解压缩,而不仅仅是验证我们在调用正确的方法。
从理论上讲,这似乎是一个集成测试,因为我们取决于可能更改的特定细节(文件系统)。
我会将与操作系统相关的代码抽象为它自己的模块(类,程序集,jar等)。在情况下,我们希望加载特定的DLL(如果找到),因此创建一个IDllLoader接口和DllLoader类。应用程序是否已使用该接口从DllLoader获取DLL并测试了..毕竟我们对解压缩代码不承担任何责任?
确实没有错,这只是我们将其称为单元测试还是集成测试的问题。我们只需要确保与文件系统进行交互就不会有意外的副作用。具体来说,请确保自己清除后-删除创建的所有临时文件-并且不要意外覆盖与文件名与使用的临时文件相同的现有文件。始终使用相对路径,而不要使用绝对路径。
在运行测试之前将chdir()放入临时目录,然后再返回chdir()也是一个好主意。
正如其他人所说,第一个可以作为集成测试。第二个仅测试功能应该实际执行的功能,而这正是单元测试应该执行的所有功能。
如图所示,第二个示例似乎毫无意义,但确实为我们提供了测试功能如何响应任何步骤中的错误的机会。在示例中,我们没有进行任何错误检查,但是在实际系统中可能会存在错误检查,而依赖项注入将使我们可以测试对任何错误的所有响应。那么成本将是值得的。
对于单元测试,我建议我们在项目中包含测试文件(EAR文件或者等效文件),然后在单元测试中使用相对路径,即" ../testdata/testfile"。
只要项目正确导出/导入,单元测试就可以正常工作。
Yay! Now it's testable; I can feed in test doubles (mocks) to the DoIt method. But at what cost? I've now had to define 3 new interfaces just to make this testable. And what, exactly, am I testing? I'm testing that my DoIt function properly interacts with its dependencies. It doesn't test that the zip file was unzipped properly, etc.
我们已经碰到了它的头上的钉子。我们要测试的是方法的逻辑,不一定要确定是否可以处理真实文件。我们不需要测试(在此单元测试中)文件是否已正确解压缩,方法将其视为理所当然。这些接口本身很有价值,因为它们提供了我们可以针对其进行编程的抽象,而不是隐式或者显式地依赖于一种具体的实现。