ChatGPT解决这个技术问题 Extra ChatGPT

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

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

人们说不要在单元测试中写入文件系统,因为如果你想写入文件系统,你就不会理解什么是单元测试。单元测试通常与单个真实对象(被测单元)交互,并且所有其他依赖项都被模拟并传入。测试类然后由测试方法组成,这些测试方法通过对象的方法验证逻辑路径,并且只有逻辑路径在被测单元。
在您的情况下,唯一需要单元测试的部分是 myDll.InvokeSomeSpecialMethod();,您将在其中检查它在成功和失败情况下是否都能正常工作,因此我不会对 DoIt 进行单元测试,但 DllRunner.Run 表示滥用 UNIT 测试仔细检查整个过程是否正常工作是可以接受的误用,因为这将是伪装单元测试的集成测试,因此不需要严格应用正常的单元测试规则

a
andreas buykx

耶!现在它可以测试了;我可以将测试替身(模拟)输入 DoIt 方法。但代价是什么?我现在必须定义 3 个新接口才能使其可测试。我到底在测试什么?我正在测试我的 DoIt 函数是否与其依赖项正确交互。它不会测试 zip 文件是否已正确解压缩等。

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


如前所述,可测试的 DoIt 函数甚至不需要测试。正如提问者正确指出的那样,没有什么重要的东西可以测试了。现在需要测试的是 IZipperIFileSystemIDllRunner 的实现,但它们正是为了测试而模拟出来的东西!
L
Lii

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

“我到底要测试什么?”

您的示例不是很有趣,因为它只是将一些 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请参阅 Black Box Testing 了解更多信息。

WHAT 是您的方法的名称(或者至少应该是)。 HOW 是你方法中的所有小实现细节。好的测试可以让你在不破坏 WHAT 的情况下更换 HOW。

这样想,问问自己:

“如果我更改此方法的实施细节(不更改公共合同)会破坏我的测试吗?”

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

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


我不确定我是否同意你在这里的信息。你是说这种方法不需要单元测试?所以你基本上是说TDD不好?就像你做 TDD 一样,如果不先写一个测试,你就不能写这个方法。或者您是否相信您的方法不需要测试?所有单元测试框架都包含“验证”功能的原因是可以使用它。 “这很糟糕,因为现在每次更改方法的实现细节时都必须更改测试”......欢迎来到单元测试的世界。
你应该测试一个方法的合同,而不是它的实现。如果您每次执行该合同时都必须更改测试,那么您将在维护应用程序代码库和测试代码库的过程中度过一段可怕的时光。
@Ronnie 盲目地应用单元测试是没有帮助的。项目的性质千差万别,单元测试并非对所有项目都有效。举个例子,我正在做一个项目,其中 95% 的代码都是关于副作用的(注意,这种副作用很重的性质是根据要求,它是基本的复杂性,而不是偶然的,因为它从种类繁多的有状态源并以很少的操作呈现它,因此几乎没有任何纯逻辑)。单元测试在这里无效,集成测试有效。
副作用应该被推到系统的边缘,它们不应该在整个层中交织在一起。在边缘测试副作用,即行为。在其他任何地方,您都应该尝试使用没有副作用的纯函数,这些函数易于测试,易于推理、重用和组合。
很好的解释,但“对于您的 DoIt() 方法,这将更适合使用集成测试进行测试。”假设您将根据执行集成测试时仍处于这种情况的实现来更改测试:“恭喜,您基本上将 DoIt() 方法的实现细节复制粘贴到测试中。快乐维护。”。那么,如果集成测试在实现更改后会中断,那么进行集成测试有什么意义呢?情况变成了类似于“进行糟糕的测试实际上比根本没有测试更糟糕”的情况
A
Adam Rosenfield

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

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


+1,但请注意,chdir() 是进程范围的,因此如果您的测试框架或其未来版本支持,您可能会破坏并行运行测试的能力。
T
Tim Cooper

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

我对此的看法是,您的单元测试会尽其所能,但可能不是 100% 的覆盖率。事实上,它可能只有 10%。关键是,你的单元测试应该很快并且没有外部依赖。他们可能会测试诸如“当您为此参数传入 null 时,此方法会引发 ArgumentNullException”之类的案例。

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

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


是的,我听到了。在这个奇异的世界里,你已经解耦了这么多,剩下的就是对抽象对象的方法调用。通风的绒毛。当你达到这一点时,你不会觉得你真的在测试任何真实的东西。您只是在测试类之间的交互。
这个答案是错误的。单元测试不像糖霜,它更像是糖。它被烤进了蛋糕里。这是编写代码的一部分……一种设计活动。因此,您永远不会用任何会“促进测试”的东西“污染”您的代码,因为测试有助于您编写代码。 99% 的情况下,测试很难编写,因为开发人员在测试之前编写了代码,并最终编写了 evil untestable code
@Christopher:为了扩展你的类比,我不希望我的蛋糕最终像香草片那样我可以使用糖。我所提倡的只是实用主义。
@Christopher:您的简历说明了一切:“我是 TDD 狂热者”。另一方面,我是务实的。我在适合的地方做 TDD,而不是在不适合的地方做 - 我的回答中没有任何内容表明我不做 TDD,尽管你似乎认为它会做。而且不管是不是TDD,为了方便测试,我不会引入大量的复杂性。
@ChristopherPerry您能解释一下如何以TDD方式解决OP的原始问题吗?我一直遇到这个;我需要编写一个函数,其唯一目的是执行具有外部依赖性的操作,就像在这个问题中一样。因此,即使在先编写测试的情况下,该测试甚至会是什么?
J
JC.

打文件系统没有错,只需将其视为集成测试而不是单元测试。我会用相对路径交换硬编码路径,并创建一个 TestData 子文件夹来包含单元测试的 zip。

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

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


它们运行的频率无关紧要。我们使用自动为我们运行它们的持续集成服务器。我们真的不在乎他们需要多长时间。如果“运行多长时间”不是问题,是否有任何理由区分单元测试和集成测试?
并不真地。但是,如果开发人员想要在本地快速运行所有单元测试,那么有一种简单的方法来做到这一点很不错。
n
nsayer

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


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

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

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


S
Sunny Milenov

假设“文件系统交互”在框架本身中得到了很好的测试,创建你的方法来处理流,并测试它。打开 FileStream 并将其传递给方法可以忽略不计,因为 FileStream.Open 已由框架创建者进行了很好的测试。


您和 nsayer 的建议基本相同:让我的代码与流一起使用。关于将流内容解压缩为 dll 文件、打开该 dll 并在其中调用函数的部分怎么样?你会在那里做什么?
@JudahHimango。这些部分不一定是可测试的。你不能测试一切。将不可测试的组件抽象为它们自己的功能块,并假设它们可以工作。当你遇到这个块的工作方式的错误时,然后为它设计一个测试,瞧。单元测试并不意味着您必须测试所有内容。在某些情况下,100% 的代码覆盖率是不现实的。
D
Dror Helper

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


D
David Sykes

正如其他人所说,第一个作为集成测试很好。第二个只测试函数应该实际做的事情,这是单元测试应该做的所有事情。

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


J
James Anderson

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

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


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅