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


138

我正在编写一个组件,给定一个ZIP文件,该组件需要:

  1. 解压缩文件。
  2. 在解压缩的文件中找到一个特定的dll。
  3. 通过反射加载该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或本机代码。


8
人们说不要在单元测试中写文件系统,因为如果您想写文件系统,那么您将不了解什么构成了单元测试。单元测试通常与单个真实对象(被测单元)进行交互,并且所有其他依赖项都被模拟并传递。然后,测试类包含测试方法,这些方法通过对象的方法验证逻辑路径,并且仅验证对象中的逻辑路径。被测单元。
Christopher Perry

1
在您的情况下,唯一需要进行单元测试的部分是myDll.InvokeSomeSpecialMethod();您将检查它在成功和失败情况下均能正常工作,因此我不会进行单元测试,DoIt但是DllRunner.Run说滥用UNIT测试来再次检查整个过程是否正常将是可以接受的滥用,并且因为它可能是伪装单元测试的集成测试,所以不需要严格地应用正常的单元测试规则
MikeT 18'Apr

Answers:


47

这样做确实没有错,这只是您将其称为单元测试还是集成测试的问题。您只需要确保与文件系统进行交互就不会有意外的副作用。具体而言,请确保自己清理后-删除创建的所有临时文件-并且不要意外覆盖与文件名相同的现有文件。始终使用相对路径,而不要使用绝对路径。

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


27
+1,但是请注意,它chdir()是整个过程的,因此,如果您的测试框架或其未来版本支持并行运行测试,则可能会破坏它的功能。

69

好极了!现在可以测试了;我可以将测试双打(模拟)输入DoIt方法。但是要花多少钱呢?我现在必须定义3个新接口才能使其可测试。我到底在测试什么?我正在测试我的DoIt函数与其依赖项正确交互。它无法测试zip文件是否已正确解压缩,等等。

您已经碰到了它的头。您要测试的是方法的逻辑,不一定要确定是否可以处理真实文件。您不需要测试(在此单元测试中)文件是否已正确解压缩,您的方法将其视为理所当然。这些接口本身很有价值,因为它们提供了您可以针对其进行编程的抽象,而不是隐式或显式地依赖于一种具体的实现。


12
所述的可测试DoIt功能甚至不需要测试。正如发问者正确指出的那样,没有什么值得检验的意义了。现在,它的实施IZipperIFileSystem以及IDllRunner需要进行测试,但他们已经嘲笑了用于测试的非常的事情!
伊恩·戈德比2014年

56

您的问题向刚接触开发人员的测试暴露了最困难的部分之一:

“我到底要测试什么?”

您的示例不是很有趣,因为它只是将一些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()方法的实现细节复制粘贴到了测试中。维护愉快。

当你写测试,要考什么,而不是如何 有关更多信息,请参见黑匣子测试

什么是你的方法的名称(或至少应该是)。该如何为所有的小细节时住在你的方法内。良好的测试让你换出如何不破坏什么

这样考虑一下,问自己:

“如果我更改此方法的实现细节(而不更改公共合同),是否会破坏我的测试?”

如果答案是肯定的,你正在测试的如何,而不是什么

为了回答有关测试具有文件系统依赖性的代码的特定问题,假设您对文件进行了一些更有趣的操作,并且想要将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()方法,将使用集成测试对其进行更适当的测试


1
我不确定我是否同意你在这里的信息。您是说不需要对这种方法进行单元测试吗?所以您基本上是说TDD不好?就像您执行TDD一样,您必须先编写测试才能编写此方法。还是要依靠一种预感,即您的方法不需要测试?所有单元测试框架都包含“验证”功能的原因是可以使用它。“这很糟糕,因为现在每次更改方法的实现细节时都必须更改测试。” ...欢迎来到单元测试领域。
罗尼2015年

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

@Ronnie盲目地应用单元测试没有帮助。有一些性质各异的项目,而单元测试并非对所有项目都有效。举例来说,我正在一个项目中,其中95%的代码都是关于副作用的(请注意,这种副作用繁重的性质是按需提供的,它是本质上的复杂性,而不是偶然的,因为它从中收集数据各种各样的有状态源,并且只需很少的操作即可呈现,因此几乎没有任何纯逻辑)。单元测试是不是有效的这里,集成测试。
Vicky Chijwani

副作用应推到系统的边缘,而不应在各个层次上相互交织。在边缘测试副作用,即行为。在其他任何地方,您都应尝试具有没有副作用的纯函数,这些函数易于测试,并且易于推理,重用和编写。
Christopher Perry

24

我不愿使用仅用于促进单元测试的类型和概念来污染我的代码。当然,如果它可以使设计更干净,更好,那就更好了,但我认为通常并非如此。

我对此的看法是,您的单元测试将尽其所能,这可能不是100%的覆盖率。实际上,可能只有10%。关键是,您的单元测试应该快速并且没有外部依赖性。他们可能会测试类似“这种方法,当您为此参数传入null时,此方法将引发ArgumentNullException”。

然后,我将添加可以具有外部依赖性并测试诸如此类的端到端方案的集成测试(也是自动化的,并且可能使用相同的单元测试框架)。

在测量代码覆盖率时,我同时测量单元测试和集成测试。


5
是的,我听到你了。在这个奇异的世界中,您已经解耦了很多,以至于剩下的就是抽象对象上的方法调用。通风的绒毛。当您达到这一点时,您似乎并没有真正在测试任何真实的东西。您只是在测试类之间的交互。
Judah Gabriel Himango

6
这个答案是错误的。单元测试不像糖霜,它更像糖。它烤成了蛋糕。这是编写代码的一部分……设计活动。因此,您绝不会用“有助于测试”的任何东西“污染”您的代码,因为测试可以帮助您编写代码。99%的时间难以编写测试,因为开发人员在测试之前编写了代码,并最终编写了无法测试的邪恶代码
Christopher Perry

1
@克里斯托弗:为了扩展您的比喻,我不希望我的蛋糕最终像香草切片一样,以便我可以用糖。我只主张实用主义。
肯特·布加亚特

1
@克里斯托弗:您的简历说了一切:“我是TDD狂热者”。另一方面,我是务实的。我在适合的地方进行TDD,而不是在不适合的地方进行TDD-尽管您似乎认为这样做,但我的回答中没有任何内容表明我不进行TDD。而且,无论是否为TDD,为了方便测试,我都不会介绍大量的复杂性。
肯特·布加亚特

3
@ChristopherPerry您能解释一下如何以TDD方式解决OP的原始问题吗?我一直都遇到这个问题;我需要编写一个函数,其唯一目的是执行具有外部依赖项的操作,例如本问题。因此,即使在先编写测试的情况下,该测试甚至是什么?
达克斯·福尔

8

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

如果您的集成测试花费的时间太长,请把它们分开,这样它们就不会像您的快速单元测试那样频繁地运行。

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


他们多久跑一次就无关紧要了。我们使用一个持续集成服务器来为我们自动运行它们。我们真的不在乎他们需要多长时间。如果不必担心“运行多长时间”,是否有任何理由需要区分单元测试和集成测试?
Judah Gabriel Himango

4
并不是的。但是,如果开发人员希望在本地快速运行所有单元测试,那么有一种简便的方法就可以了。
JC。

6

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


好的,这样就可以注入流了。依赖注入/ IOC。将流解压缩到文件中,在这些文件中加载dll并在该dll中调用方法的部分怎么样?
Judah Gabriel Himango

3

从理论上讲,这似乎是一个集成测试,因为您取决于可能更改的特定细节(文件系统)。

我会将与操作系统相关的代码抽象为它自己的模块(类,程序集,jar等)。在您的情况下,您希望加载特定的DLL(如果找到),因此创建一个IDllLoader接口和DllLoader类。您的应用程序是否已使用该接口从DllLoader获取DLL并测试了..毕竟您不对解压缩代码负责吗?


2

假设“文件系统交互”已在框架本身中进行了很好的测试,请创建用于流的方法并进行测试。由于框架创建者已经对FileStream.Open进行了良好的测试,因此可以将打开FileStream并将其传递给该方法的测试忽略掉。


您和nsayer的建议基本相同:使我的代码与流一起使用。将流内容解压缩到dll文件,打开该dll并在其中调用函数的部分怎么样?你在那里做什么?
Judah Gabriel Himango '09年

3
@JudahHimango。这些部分可能不一定可以测试。您无法测试所有内容。将不可测试的组件抽象到它们自己的功能块中,并假定它们可以工作。当您遇到此块的工作方式错误时,请为此进行测试,然后瞧。单元测试并不意味着您必须测试所有内容。在某些情况下,100%的代码覆盖率是不现实的。
Zoran Pavlovic

1

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


1

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

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


0

正如其他人所说,第一个可以作为集成测试。第二个测试仅测试功能应该实际执行的功能,而这是单元测试应执行的全部操作。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.