我写一个组成部分,给定一个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层新的界面,使这个测试。 而且,究竟是什么,我在测试? 我测试我的大一正常工作与它的依赖交互。 它不测试zip文件被正确地解压缩等。
它不觉得我了测试功能。 这感觉就像我只是测试类的相互作用。
我的问题是 :什么是单元测试的东西的正确方式,依赖于文件系统?
编辑我使用.NET,但这个概念可以适用Java或本机代码了。
确实没有什么错,它只是一个你是否把它叫做一个单元测试或集成测试的问题。 你只需要确保,如果你与文件系统进行交互,没有意外的副作用。 特别是,确保你之后youself清理 - 删除您创建的临时文件 - 和你不小心覆盖该碰巧有相同的文件名,你使用的临时文件已存在的文件。 始终使用相对路径,而不是绝对路径。
这也将是一个好主意, chdir()
到一个临时目录中运行测试,以及前chdir()
回来之后。
好极了! 现在,它的可检验的; 我可以在测试双打(嘲笑)到DOIT方法饲料。 但代价是什么? 现在我已经定义了3层新的界面,使这个测试。 而且,究竟是什么,我在测试? 我测试我的大一正常工作与它的依赖交互。 它不测试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字。 参见黑盒测试更多。
在你有什么方法的名称(或至少应该是)。 该如何所有的小细节时住在你的方法内。 良好的测试让你换出如何在不破坏的东西 。
想想看,这样一来,问问自己:
“如果我改变了方法的实现细节(不改变公共合同),将它破坏我的测试(S)?”
如果答案是肯定的,你正在测试的如何 ,而不是什么 。
要回答您对于使用文件系统的依赖性测试代码的具体问题,让我们说你有一些更有趣的同一个文件怎么回事,你想一个的Base64编码的内容保存byte[]
到文件中。 您可以使用流为这个来测试你的代码做正确的事,而不必检查是怎样做的。 一个例子可能是这样的(在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)将返回FileOutputStream
从outputStream()
并会写入File
。
什么是关于有趣的write
在这里的方法是,在写编码的内容进行Base64编码,所以这就是我们测试了。 为了您的DoIt()
方法,这将是更适当地与测试集成测试 。
我不想透露我的污染与类型和存在仅仅是为了便于单元测试代码的概念。 当然,如果它使设计更清洁,更再大,但我认为这是往往并非如此。
我对这个问题的看法是你的单元测试会做多,因为他们可以这可能不是100%的覆盖率。 事实上,它可能只有10%。 问题是,你的单元测试要快而没有外部依赖。 像“这种方法,当你在空为该参数传递引发ArgumentNullException”他们可能会测试案例。
然后我将增加集成测试(也自动化,并且可能使用相同的单元测试框架),其可具有外部的依赖和测试端至端方案,例如这些。
当测量代码覆盖率,我同时测量单元和集成测试。
有没有错,碰到文件系统,只是认为这是一个集成测试,而不是单元测试。 我换了硬编码路径使用相对路径,并创建一个子文件夹TESTDATA包含了单元测试的拉链。
如果你的集成测试花费太长的时间来运行,那么所以他们不是经常为你的快速单元测试运行它们分离出来。
我同意,有时候我觉得互动为基础的测试可能会导致过多的耦合,往往最终不能提供足够的价值。 你真的想测试在这里解压文件不只是验证您所呼叫的正确方法。
一种方式是写解压方法采取InputStreams。 然后,单元测试可以构建使用ByteArrayInputStream的一个字节数组这样的InputStream。 该字节数组的内容可以是在单元测试代码的常数。
这似乎是更因为你是依赖于特定的细节(文件系统)可能会改变,从理论上讲的集成测试。
我将抽象与操作系统涉及到它自己的模块(类,组件,罐子,不管)的代码。 你的情况要加载特定DLL如果找到了,所以做出IDllLoader接口和DllLoader类。 有您的应用程序获得使用..你不负责的解压缩代码毕竟右边的接口和测试从DllLoader的DLL?
假设“文件系统交互”的框架本身是行之有效的,创建方法与流工作,并进行测试。 打开一个FileStream,并把它传递给方法可以去掉你的测试,如FileStream.Open深受框架创建者进行测试。
你不应该测试类交互和功能调用。 相反,你应该考虑集成测试。 测试所需的结果,而不是文件加载操作。
对于单元测试,我建议你在项目中包含(EAR文件或同等学历)的测试文件,然后在单元测试,即“../testdata/testfile”使用相对路径。
只要你的项目是正确的出口/进口比你的单元测试应该工作。
正如其他人所说,第一个是罚款,一个集成测试。 第二只测试什么功能应该真正做到,这是所有单元测试应该做的。
如图所示,第二个例子看起来有点毫无意义,但它给你的机会来测试功能如何响应任何步骤的错误。 您没有任何错误的例子检查,但在实际系统中你可能有,和依赖注入将让你测试所有的任何错误的响应。 那么成本将是值得的。