首页 文章

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

提问于
浏览
117

我正在编写一个组件,给定一个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文件是否正确解压缩等 .

我觉得我不再测试功能了 . 感觉就像我只是在测试课堂互动 .

My question is this :单元测试依赖于文件系统的东西的正确方法是什么?

编辑我正在使用.NET,但这个概念也可以应用Java或本机代码 .

11 回答

  • 1

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

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

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

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

  • 6

    对于单元测试,我建议您在项目中包含测试文件(EAR文件或等效文件),然后在单元测试中使用相对路径,即“../testdata/testfile” .

    只要您的项目正确导出/导入,您的单元测试应该可以正常工作 .

  • 22

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

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

  • 53

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

  • 3

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

    "What the hell do I test?"

    你的例子不是很有趣,因为它只是粘合了一些API调用,所以如果你要为它编写一个单元测试,你最终会断言调用方法 . 像这样的测试将您的实现细节紧密地结合到测试中 . 这很糟糕,因为现在每次更改方法的实现细节时都必须更改测试,因为更改实现细节会破坏您的测试!

    Having bad tests is actually worse than having no tests at all.

    在你的例子中:

    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() 方法的实现细节复制粘贴到测试中 . 快乐的维护 .

    When you write tests you want to test the WHAT and not the HOW. 有关更多信息,请参阅Black Box Testing .

    WHAT 是您的方法的名称(或至少它应该是) . HOW 是您方法中的所有小实现细节 . 良好的测试允许您在不破坏 WHAT 的情况下换出 HOW .

    想一想,问问自己:

    "If I change the implementation details of this method (without altering the public contract) will it break my test(s)?"

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

    要回答有关使用文件系统依赖性测试代码的具体问题,假设您对文件进行了一些更有趣的事情,并且希望将 byte[] 的Base64编码内容保存到文件中 . 您可以使用流来测试您的代码是否正确,而无需检查 how 它是否正确 . 一个例子可能是这样的(在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进行更合适的测试 .

  • 0

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

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

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

  • 8

    这似乎更像是一个集成测试,因为你依赖于理论上可能改变的特定细节(文件系统) .

    我会将处理OS的代码抽象到它自己的模块(类,汇编,jar,等等)中 . 在您的情况下,如果找到,则要加载特定的DLL,因此请创建一个IDllLoader接口和DllLoader类 . 让你的应用程序使用界面从DllLoader获取DLL并测试..你不负责解压缩代码吗?

  • 1

    你不应该测试类交互和函数调用 . 相反,你应该考虑集成测试 . 测试所需的结果,而不是文件加载操作 .

  • 41

    正如其他人所说,第一个是集成测试 . 第二个测试只测试函数实际应该做什么,这是单元测试应该做的全部 .

    如图所示,第二个示例看起来有点无意义,但它确实让您有机会测试函数如何响应任何步骤中的错误 . 您没有在示例中进行任何错误检查,但在您可能拥有的实际系统中,依赖注入将允许您测试对任何错误的所有响应 . 那么成本将是值得的 .

  • 2

    假设“文件系统交互”在框架本身进行了很好的测试,请创建使用流的方法,并对其进行测试 . 打开FileStream并将其传递给方法可能会被排除在测试之外,因为FileStream.Open已经过框架创建者的充分测试 .

  • 37

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

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

相关问题