unit testing代码与文件系统的依赖关系

我写了一个组件,给定一个ZIP文件,需要:

  1. 解压该文件。
  2. 在解压缩的文件中find一个特定的dll。
  3. 通过reflection加载该dll并调用其上的一个方法。

我想unit testing这个组件。

我很想编写直接处理文件系统的代码:

void DoIt() { Zip.Unzip(theZipFile, "C:\\foo\\Unzipped"); System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar"); myDll.InvokeSomeSpecialMethod(); } 

但人们经常说:“不要编写依赖文件系统,数据库,networking等的unit testing”

如果我以unit testing友好的方式写这个,我想这应该是这样的:

 void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner) { string path = zipper.Unzip(theZipFile); IFakeFile file = fileSystem.Open(path); runner.Run(file); } 

好极了! 现在是可testing的; 我可以喂testing双打(嘲笑)的DoIt方法。 但费用是多less? 我现在不得不定义3个新的接口来做这个testing。 我究竟在testing什么? 我正在testing我的DoIt函数是否正确地与它的依赖进行交互。 它不testingzip文件是否正确解压缩等。

它不觉得我正在testingfunction了。 感觉就像我只是在testing课堂互动。

我的问题是 :什么是正确的方式来unit testing依赖于文件系统的东西?

编辑我正在使用.NET,但是这个概念也可以应用Java或本地代码。

这真的没有什么问题,这只是一个问题,你称之为unit testing还是集成testing。 您只需确保如果您与文件系统进行交互,就不会有意想不到的副作用。 具体来说,确保你自己清理完毕后删除你创build的临时文件,而且不会意外地覆盖一个已经存在的文件,这个文件和你正在使用的临时文件具有相同的文件名。 始终使用相对path,而不是绝对path。

在运行你的testing之前把chdir()放到一个临时目录中,然后把chdir()放回去也是一个好主意。

好极了! 现在是可testing的; 我可以喂testing双打(嘲笑)的DoIt方法。 但费用是多less? 我现在不得不定义3个新的接口来做这个testing。 我究竟在testing什么? 我正在testing我的DoIt函数是否正确地与它的依赖进行交互。 它不testingzip文件是否正确解压缩等。

你已经把它钉在了头上。 你想testing的是你的方法的逻辑,而不是一个真正的文件是否可以解决。 你不需要testing(在这个unit testing中)一个文件是否被正确地解压缩,你的方法是理所当然的。 这些接口本身是有价值的,因为它们提供了可以编程的抽象,而不是隐式地或明确地依赖于一个具体的实现。

您的问题暴露了开发人员进行testing最难的部分之一:

“我到底在testing什么?”

你的例子不是很有趣,因为它只是把一些API调用粘在一起,所以如果你要为它编写一个unit testing,你最终只会声明方法被调用。 像这样的testing将您的实现细节紧密结合到testing中。 这很糟糕,因为现在每次更改方法的实现细节时都必须更改testing,因为更改实现细节会破坏您的testing(s)!

不好的testing实际上比根本没有testing更糟糕。

在你的例子中:

 void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner) { string path = zipper.Unzip(theZipFile); IFakeFile file = fileSystem.Open(path); runner.Run(file); } 

虽然你可以通过模拟,但是testing方法没有逻辑。 如果你想为此尝试一个unit testing,它可能看起来像这样:

 // Assuming that zipper, fileSystem, and runner are mocks void testDoIt() { // mock behavior of the mock objects when(zipper.Unzip(any(File.class)).thenReturn("some path"); when(fileSystem.Open("some path")).thenReturn(mock(IFakeFile.class)); // run the test someObject.DoIt(zipper, fileSystem, runner); // verify things were called verify(zipper).Unzip(any(File.class)); verify(fileSystem).Open("some path")); verify(runner).Run(file); } 

恭喜,您基本上将DoIt()方法的实现细节复制粘贴到testing中。 快乐的维护。

当你写testing时,你想testing什么而不是如何 有关更多信息,请参阅黑盒testing

什么是你的方法的名称(或者至less应该是)。 HOW是你的方法中的所有小实现细节。 好的testing可以让你在不破坏什么的情况下更换HOW

想想这样,问自己:

“如果我改变这个方法的实施细节(不改变公共合同),它会不会破坏我的testing?”

如果答案是肯定的,那么你正在testing如何而不是什么

为了回答关于testing具有文件系统依赖关系的代码的具体问题,让我们假设你对一个文件进行了一些更有趣的事情,并且你想将一个byte[]的Base64编码内容保存到一个文件中。 您可以使用stream来testing您的代码是否正确,而不必检查它是如何执行的。 一个例子可能是这样的(在Java中):

 interface StreamFactory { OutputStream outStream(); InputStream inStream(); } class Base64FileWriter { public void write(byte[] contents, StreamFactory streamFactory) { OutputStream outputStream = streamFactory.outStream(); outputStream.write(Base64.encodeBase64(contents)); } } @Test public void save_shouldBase64EncodeContents() { OutputStream outputStream = new ByteArrayOutputStream(); StreamFactory streamFactory = mock(StreamFactory.class); when(streamFactory.outStream()).thenReturn(outputStream); // Run the method under test Base64FileWriter fileWriter = new Base64FileWriter(); fileWriter.write("Man".getBytes(), streamFactory); // Assert we saved the base64 encoded contents assertThat(outputStream.toString()).isEqualTo("TWFu"); } 

testing使用ByteArrayOutputStream但在应用程序中(使用dependency injection),真正的StreamFactory(也许称为FileStreamFactory)将从outputStream()返回FileOutputStream并写入File

这里write方法的有趣之处在于它将内容写出Base64编码,所以这就是我们testing的内容。 对于你的DoIt()方法,这将通过集成testing进行更恰当的testing 。

我沉溺于污染我的代码的types和概念,只是为了方便unit testing。 当然,如果它使devise更清洁,更好,那么很好,但我认为往往不是这样。

我认为这是你的unit testing将做尽可能多的,可能不是100%的覆盖率。 实际上,可能只有10%。 重点是,你的unit testing应该是快速的,没有外部依赖。 他们可能会testing像“这个方法抛出一个ArgumentNullException当你为这个parameter passingnull”的情况。

然后,我会添加集成testing(也是自动化的,可能使用相同的unit testing框架),这些testing可能具有外部依赖性,并testing端到端情况(如这些)。

测量代码覆盖率时,我测量单元和集成testing。

点击文件系统没有任何问题,只要考虑它是一个集成testing,而不是unit testing。 我会交换硬编码的path与相对path,并创build一个TestData子文件夹来包含unit testing的拉链。

如果你的集成testing需要很长的时间才能运行,那么将它们分开,这样它们就不会像你的快速unit testing那样频繁运行。

我同意,有时我认为基于交互的testing可能会导致太多的耦合,并且往往不能提供足够的价值。 你真的想在这里testing解压缩文件,不只是validation你正在调用正确的方法。

一种方法是编写解压缩方法来获取InputStreams。 然后unit testing可以使用ByteArrayInputStream从一个字节数组构造这样一个InputStream。 该字节数组的内容在unit testing代码中可以是常量。

这看起来更像是一个集成testing,因为在理论上,取决于具体的细节(文件系统),这可能会发生变化。

我会将处理操作系统的代码抽象到它自己的模块(类,程序集,jar,等等)中。 在你的情况下,你想要加载一个特定的DLL,如果find,所以做一个IDllLoader接口和DllLoader类。 让你的应用程序使用接口从DllLoader获取DLL,然后testing..你不负责解压缩代码。

假设“文件系统交互”在框架本身已经过很好的testing,创build你的方法来处理stream,并testing它。 由于FileStream.Open由框架创build者进行了充分的testing,因此打开FileStream并将其传递给方法可以不在testing中。

你不应该testing类的交互和函数调用。 相反,你应该考虑集成testing。 testing所需的结果,而不是文件加载操作。

对于unit testing,我build议你在你的项目中包含testing文件(EAR文件或等价的),然后在unit testing中使用相对path,即“../testdata/testfile”。

只要您的项目正确导出/导入比您的unit testing应该工作。

正如其他人所说,第一个作为一个整合testing是好的。 第二个testing只是function应该实际做什么,这是一个unit testing应该做的。

如图所示,第二个例子看起来有点没有意义,但它确实给了你机会来testing函数如何响应任何步骤中的错误。 在这个例子中你没有任何错误检查,但是在真实的系统中你可能有,并且dependency injection可以让你testing所有错误的响应。 那么成本将是值得的。