单元测试具有文件系统依赖性的代码
-
02-07-2019 - |
题
我正在编写一个组件,给定一个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或本机代码。
解决方案
这没有什么不妥,这只是一个问题,你是将它称为单元测试还是集成测试。您只需要确保如果您与文件系统进行交互,就不会出现意外的副作用。具体来说,请确保在自己之后清理 - 删除您创建的任何临时文件 - 并且您不会意外覆盖与您正在使用的临时文件具有相同文件名的现有文件。始终使用相对路径而不是绝对路径。
在运行测试之前将 chdir()
放入临时目录,然后再返回 chdir()
也是个好主意。
其他提示
耶!现在它是可测试的;我可以将测试双打(模拟)提供给DoIt方法。但是以什么代价?我现在必须定义3个新接口才能使这个可测试。究竟,我在测试什么?我正在测试我的DoIt函数是否正确地与其依赖项进行交互。它不会测试zip文件是否正确解压缩等等。
你的头上钉了一针。您要测试的是您的方法的逻辑,而不一定是否可以处理真正的文件。您不需要测试(在此单元测试中)文件是否正确解压缩,您的方法认为这是理所当然的。接口本身很有价值,因为它们提供了可以编程的抽象,而不是隐式或明确地依赖于一个具体的实现。
您的问题暴露了开发人员刚刚进入测试中最困难的部分之一:
“我该怎么办?”
你的例子不是很有趣,因为它只是粘合了一些API调用,所以如果你要为它编写单元测试,你最终会断言调用方法。像这样的测试将您的实现细节与测试紧密结合在一起。这很糟糕,因为现在每次更改方法的实现细节时都必须更改测试,因为更改实现细节会破坏您的测试!
测试不好实际上比完全没有测试更糟糕。
在你的例子中:
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()
方法的实现细节复制粘贴到测试中。快乐的维护。
当你编写测试时,你想测试 WHAT 而不是 HOW 。 有关详情,请参见黑匣子测试。
WHAT 是您的方法的名称(或至少它应该是)。 HOW 是您方法中存在的所有小实现细节。良好的测试允许您在不破坏 WHAT 的情况下更换 HOW 。
这样想一想,问问自己:
"如果我更改此方法的实施细节(不改变公共合同)会破坏我的测试吗?
如果答案是肯定的,那么您正在测试如何,而不是 WHAT 。
要回答有关使用文件系统依赖性测试代码的特定问题,假设您对文件进行了一些更有趣的事情并希望保存 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)将从 outputStream()<返回
FileOutputStream
/ code>并写入 File
。
这里的 write
方法的有趣之处在于它将内容写入Base64编码,因此我们测试了它。对于 DoIt()
方法,使用集成测试<进行更合适的测试/ A>
我不愿意使用仅为便于单元测试而存在的类型和概念来污染我的代码。当然,如果它使设计更清洁,更好,那么很好,但我认为通常情况并非如此。
我对此的看法是,您的单元测试会尽可能多地执行,而这可能不是100%覆盖。事实上,它可能只有10%。关键是,您的单元测试应该很快并且没有外部依赖性。他们可能会测试像“此方法在为此参数传入null时抛出ArgumentNullException”这样的情况。
然后我会添加集成测试(也是自动化的,可能使用相同的单元测试框架),这些测试可以具有外部依赖性并测试这些端到端场景。
在测量代码覆盖率时,我会测量单位和积分测试。
点击文件系统没有任何问题,只需将其视为集成测试而不是单元测试。我将硬编码路径与相对路径交换,并创建一个TestData子文件夹,以包含单元测试的拉链。
如果您的集成测试需要很长时间才能运行,那么请将它们分开,以免它们像您的快速单元测试那样频繁运行。
我同意,有时我认为基于交互的测试会导致过多的耦合,并且往往最终无法提供足够的价值。你真的想在这里测试解压缩文件而不仅仅是验证你正在调用正确的方法。
一种方法是编写解压缩方法来获取InputStreams。然后单元测试可以使用ByteArrayInputStream从字节数组构造这样的InputStream。该字节数组的内容可以是单元测试代码中的常量。
这似乎更像是一个集成测试,因为你依赖于理论上可能改变的特定细节(文件系统)。
我会将处理操作系统的代码抽象到它自己的模块(类,汇编,jar,等等)中。在您的情况下,如果找到,则要加载特定的DLL,因此请创建一个IDllLoader接口和DllLoader类。让你的应用程序使用界面从DllLoader获取DLL并测试..你不负责解压缩代码吗?
假设“文件系统交互”在框架本身进行了很好的测试,创建了使用流的方法,并测试了它。打开FileStream并将其传递给方法可能会被排除在测试之外,因为FileStream.Open已经过框架创建者的充分测试。
您不应该测试类交互和函数调用。相反,你应该考虑集成测试。测试所需的结果,而不是文件加载操作。
对于单元测试,我建议您在项目中包含测试文件(EAR文件或等效文件),然后在单元测试中使用相对路径,即“../ testdata / testfile”。
只要你的项目被正确导出/导入,你的单元测试就应该有效。
正如其他人所说,第一个很好,作为整合测试。第二个测试只测试函数应该实际执行的操作,这是单元测试应该做的全部。
如图所示,第二个示例看起来有点无意义,但它确实让您有机会测试函数如何响应任何步骤中的错误。您在示例中没有任何错误检查,但在您可能拥有的实际系统中,依赖注入将允许您测试对任何错误的所有响应。那么成本将是值得的。