当前位置:  开发笔记 > 编程语言 > 正文

单元测试具有文件系统依赖性的代码

如何解决《单元测试具有文件系统依赖性的代码》经验,为你挑选了6个好方法。

我正在编写一个组件,给定一个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或本机代码.



1> andreas buyk..:

好极了!现在它是可测试的; 我可以将测试双打(模拟)提供给DoIt方法.但是以什么代价?我现在必须定义3个新接口才能使这个可测试.究竟,我在测试什么?我正在测试我的DoIt函数是否正确地与其依赖项交互.它不测试zip文件是否正确解压缩等.

你的头上钉了一针.您要测试的是您的方法的逻辑,而不一定是否可以处理真正的文件.您不需要测试(在此单元测试中)文件是否正确解压缩,您的方法认为这是理所当然的.接口本身很有价值,因为它们提供了可以编程的抽象,而不是隐式或明确地依赖于一个具体的实现.


如上所述的可测试的"DoIt"功能甚至不需要测试.正如提问者正确指出的那样,没有任何重要意义可供考验.现在它是需要测试的`IZipper`,`IFileSystem`和`IDllRunner`的实现,但它们是为测试而被模拟的东西!

2> Christopher ..:

您的问题暴露了开发人员刚刚进入测试中最困难的部分之一:

"我该怎么办?"

你的例子不是很有趣,因为它只是粘合了一些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.

想一想,问问自己:

"如果我改变这种方法的实施细节(不改变公共合同),它会破坏我的考试吗?"

如果答案是肯定的,那么您正在测试HOW而不是WHAT.

要回答有关使用文件系统依赖性测试代码的特定问题,假设您对文件进行了一些更有趣的事情,并且希望将a的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()方法,使用集成测试可以更好地测试它.


您应该测试方法的CONTRACT,而不是方法的实现。如果每次合同的实现发生变化时都必须更改测试,那么您将不得不同时维护应用程序代码库和测试代码库。

3> Adam Rosenfi..:

这真的没什么问题,只是你把它称为单元测试还是集成测试的问题.您只需要确保如果您与文件系统进行交互,就不会出现意外的副作用.具体来说,请确保在自己之后清理 - 删除您创建的任何临时文件 - 并且您不会意外覆盖与您正在使用的临时文件具有相同文件名的现有文件.始终使用相对路径而不是绝对路径.

chdir()在运行测试之前进入临时目录并在chdir()之后返回也是一个好主意.


但请注意,`chdir()`是整个流程的,因此如果您的测试框架或其未来版本支持,您可能会破坏并行运行测试的能力.

4> Kent Boogaar..:

我很谨慎地使用仅为了便于单元测试而存在的类型和概念来污染我的代码.当然,如果它使设计更清洁,更好,那么很好,但我认为通常情况并非如此.

我对此的看法是,你的单元测试会尽可能多地完成,而这可能不是100%覆盖.事实上,它可能只有10%.关键是,您的单元测试应该很快并且没有外部依赖性.他们可能会测试类似"此方法在为此参数传入null时抛出ArgumentNullException"的情况.

然后我会添加集成测试(也是自动化的,可能使用相同的单元测试框架),这些测试可以具有外部依赖性并测试这些端到端场景.

在测量代码覆盖率时,我会测量单位和积分测试.


这个答案是错误的.单元测试不像结霜,更像是糖.它融入了蛋糕.这是编写代码的一部分......一项设计活动.因此,您永远不会使用"便于测试"的任何内容"污染"您的代码,因为测试可以帮助您编写代码.由于开发人员在测试之前编写了代码,因此99%的时间难以编写测试,最终编写了[邪恶的不可测试代码](http://googletesting.blogspot.com/2008/07/how-to-write -3v1l-不可测-code.html)
是的,我听到了你的声音.你到达的地方就是这个离奇的世界,你所有的左派都是对抽象对象的方法调用.通风绒毛.当你达到这一点时,你并不觉得你真的在测试任何真实的东西.您只是在测试类之间的交互.
@ChristopherPerry你能解释如何以TDD的方式解决OP的原始问题吗?我一直遇到这个问题; 我需要编写一个函数,其唯一目的是执行具有外部依赖的操作,就像在这个问题中一样.因此,即使在第一次编写测试的场景中,该测试甚至会是什么?

5> JC...:

点击文件系统没有任何问题,只需将其视为集成测试而不是单元测试.我将硬编码路径与相对路径交换,并创建一个TestData子文件夹以包含单元测试的拉链.

如果您的集成测试运行时间太长,则将它们分开,这样它们就不会像快速单元测试那样频繁运行.

我同意,有时我认为基于交互的测试会导致过多的耦合,并且往往最终无法提供足够的价值.你真的想在这里测试解压缩文件而不仅仅是验证你正在调用正确的方法.


并不是的.但是如果开发人员希望在本地快速运行所有单元测试,那么很容易就能做到这一点.

6> nsayer..:

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

推荐阅读
mobiledu2402851377
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有