我正在编写一个组件,给定一个ZIP文件,它需要:
我想对这个组件进行单元测试。
我很想编写直接处理文件系统的代码:
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或原生代码。
发布于 2008-09-24 19:09:32
这真的没有错,只是你把它叫做单元测试还是集成测试的问题。您只需确保在与文件系统进行交互时,不会出现意外的副作用。具体来说,请确保您在自行删除创建的任何临时文件后进行清理,并且不会意外地覆盖与您正在使用的临时文件具有相同文件名的现有文件。始终使用相对路径,而不是绝对路径。
在运行测试之前先chdir()
到一个临时目录,然后再运行chdir()
,这也是一个好主意。
发布于 2010-11-02 15:15:03
耶!现在它是可测试的;我可以将测试替身(模拟)提供给DoIt方法。但代价是什么呢?我现在必须定义3个新的接口,才能使其可测试。那么,我到底在测试什么?我正在测试我的DoIt函数是否与它的依赖项正确交互。它不会测试zip文件是否被正确解压缩,等等。
你说的一针见血。您想要测试的是您的方法的逻辑,而不一定是一个真正的文件是否可以被寻址。你不需要(在这个单元测试中)测试一个文件是否被正确解压,你的方法认为这是理所当然的。这些接口本身很有价值,因为它们提供了您可以针对其进行编程的抽象,而不是隐式或显式地依赖于一个具体的实现。
发布于 2014-04-17 12:52:55
你的问题暴露了刚开始测试的开发人员最困难的部分之一:
“我该测试什么?”
您的示例并不是很有趣,因为它只是将一些API调用粘合在一起,因此如果您要为它编写单元测试,您最终只会断言方法已被调用。像这样的测试将您的实现细节与测试紧密地结合在一起。这很糟糕,因为现在每次更改方法的实现细节时都必须更改测试,因为更改实现细节会破坏测试!
有糟糕的测试实际上比根本没有测试更糟糕。
在您的示例中:
void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
string path = zipper.Unzip(theZipFile);
IFakeFile file = fileSystem.Open(path);
runner.Run(file);
}
虽然您可以传入mock,但在要测试的方法中没有逻辑。如果您尝试对此进行单元测试,它可能如下所示:
// 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()
方法的实现细节复制粘贴到了测试中。快乐维护。
当您编写测试时,您希望测试 WHAT ,而不是 Black Box Testing,请参阅Black Box Testing了解更多信息。
是你的方法的名称(或者至少应该是)的。HOW是你的方法内部的所有小实现细节。好的测试允许您在不破坏WHAT的情况下交换出HOW。
这样想,问问你自己:
“如果我更改此方法的实现细节(不更改公共约定),是否会破坏我的测试?”
如果答案是肯定的,那么您测试的是HOW,而不是WHAT。
为了回答您关于测试带有文件系统依赖项的代码的特定问题,假设您对一个文件进行了一些更有趣的操作,并且希望将byte[]
的Base64编码内容保存到一个文件中。您可以使用streams来测试您的代码是否正确,而不必检查是如何做到这一点的。一个例子可能是这样(在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()
方法,使用integration test进行测试会更合适。
https://stackoverflow.com/questions/129036
复制相似问题