单元测试代码与文件系统依赖关系
我正在编写一个组件,给定一个ZIP文件,需要:
我想单元测试这个组件。
我很想编写直接处理文件系统的代码:
void DoIt()
{
Zip.Unzip(theZipFile, "C:fooUnzipped");
System.IO.File myDll = File.Open("C:fooUnzippedSuperSecret.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或本地代码。
这真的没有什么不对,这只是你称之为单元测试还是集成测试的问题。 您只需确保如果您与文件系统进行交互,就不会有意外的副作用。 具体而言,请确保在自己清理后删除您创建的临时文件,并且不会意外覆盖与您正在使用的临时文件具有相同文件名的现有文件。 始终使用相对路径而不是绝对路径。
在运行你的测试之前把chdir()
放到一个临时目录中也是个好主意,之后chdir()
chdir()
放回去。
好极了! 现在它是可测试的; 我可以将测试双打(嘲笑)喂给DoIt方法。 但费用是多少? 我现在必须定义3个新接口来制作这个测试。 我究竟在测试什么? 我正在测试我的DoIt函数是否正确地与它的依赖进行交互。 它不测试zip文件是否正确解压缩等。
你的头上钉了指甲。 你想测试的是你的方法的逻辑,不一定是否可以解决一个真实的文件。 您不需要测试(在本单元测试中)文件是否正确解压缩,您的方法将视为理所当然。 这些接口本身很有价值,因为它们提供了可以编程的抽象,而不是隐式地或明确地依赖于一个具体的实现。
您的问题暴露了开发人员进行测试时最难的部分之一:
“我到底在测试什么?”
你的例子不是很有趣,因为它只是将一些API调用粘在一起,所以如果你要为它编写一个单元测试,你最终只会声明方法被调用。 像这样的测试将您的实现细节紧密结合到测试中。 这很糟糕,因为现在每次更改方法的实现细节时都必须更改测试,因为更改实现细节会破坏您的测试(s)!
不好的测试实际上比根本没有测试更糟糕。
在你的例子中:
void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
string path = zipper.Unzip(theZipFile);
IFakeFile file = fileSystem.Open(path);
runner.Run(file);
}
虽然你可以通过模拟,但在测试方法中没有逻辑。 如果你想为此尝试一个单元测试,它可能看起来像这样:
// 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()
方法的实现细节复制粘贴到测试中。 快乐的维护。
当你写测试时,你想测试什么而不是如何。 有关更多信息,请参阅黑盒测试
什么是你的方法的名称(或者至少应该是)。 HOW是你的方法中的所有小实现细节。 良好的测试可以让你在不破坏什么的情况下更换HOW 。
这样想一想,问问自己:
“如果我改变这种方法的实施细节(不改变公共合同),它是否会破坏我的测试?”
如果答案是肯定的,你正在测试如何而不是什么 。
要回答关于使用文件系统依赖关系测试代码的具体问题,假设您对文件进行了一些更有趣的操作,并且希望将Base byte[]
的Base64编码内容保存到文件中。 您可以使用流来测试您的代码是否正确,而不必检查它是如何执行的。 一个例子可能是这样的(在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");
}
该测试使用ByteArrayOutputStream
但在应用程序中(使用依赖注入),实际的StreamFactory(也许称为FileStreamFactory)将从outputStream()
返回FileOutputStream
并写入File
。
这里write
方法的有趣之处在于它将内容写入Base64编码,所以这就是我们测试的内容。 对于您的DoIt()
方法,这将通过集成测试进行更恰当的测试。