Fast Tests Help Humans, Deep Tests Help Servers

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

为了揭示更高复杂度的错误,自动化测试被转化为涉及外部资源的集成测试场景,而不是使用模拟。虽然这种方法提高了测试覆盖率,但会减慢整个构建流程。这损害了自动化测试的初衷,即作为一个安全网和帮助程序员安全地编辑代码的工具。将测试分为“快速”和“深入”,然后让人工运行前者,而服务器运行后者,可能是解决这个问题的好办法。

考虑以下具有简单toString()静态方法的Java代码:

static String toString(InputStream stream) throws IOException {
  final StringBuilder buf = new StringBuilder();
  while (true) {
    int d = stream.read();
    if (d == -1) {
      break;
    }
    buf.append((char) d);
  }
  return buf.toString();
}

它逐字节读取stream,将其附加到缓冲区,并将缓冲区返回给客户端。这是验证功能的JUnit5测试。

@Test
void readsSomeData() throws IOException {
  Assertions.assertEquals(
    "ABC",
    toString(
      new ByteArrayInputStream(
        new byte[] {0x41, 0x42, 0x43}
      )
    )
  );
}

到目前为止,一切都很好。测试工作正常,方法似乎是正确的。此外,测试完成非常快,仅需要在我的笔记本电脑上5毫秒。然而,在仔细检查后,我们发现了一个方法中的错误:它没有关闭输入流。这个问题并不影响测试,因为输入流在内存中,并且不持有任何可能泄漏的有价值的资源。然而,如果我们引入一个新的测试,它将暴露这个问题。

@Test
void readsFromManyFiles(@TempDir Path tmp) throws IOException {
  Path f = tmp.resolve("test.txt");
  Files.write(f, "Hello, world!".getBytes());
  for (int i = 0; i < 20000; ++i) {
    Assertions.assertEquals(
      13,
      toString(new FileInputStream(f.toFile())).length()
    );
  }
}

当我运行这个测试时,我会得到一个显示“Too many open files”的FileNotFoundException。如果我将for循环中的上限减少到10000,错误就会消失。这肯定是因为Mac OS X上的最大打开文件数是12,288。然而,在Ubuntu上,这个限制设定为65536。因此,如果我在Ubuntu上运行测试,就不会发现错误。我相信你知道如何在toString()方法中修复这个错误。

显然,第二个测试比第一个测试慢得多,在我的笔记本电脑上需要 650 毫秒(慢了 130 倍!)。这只是一个帮助检测错误但耗时的测试的例子。通常情况下,集成测试对性能有如此负面影响是因为它们涉及到“外部”资源,而这些资源通常比较慢。第二个测试使用的文件系统就是这样的一个外部资源。

当一个年轻的项目只有几个测试方法时,650毫秒可能不会有问题。然而,随着测试数量的增加,慢速测试很快成为一个问题,因为整体构建时间变长,给程序员带来了挫败感。自动化测试本来是为了辅助编码人员,却变成了阻碍。如果一个编码人员在每次代码更改后需要等待几分钟来确保没有出现问题,那么他们会感到沮丧。通常情况下,沮丧的编码人员可能会删除那些慢速测试。

不用说,删除慢速测试并不是解决方案。那是什么呢?加速它们?并不完全是这样。加速集成测试通常是具有挑战性的,甚至有时是不可能的,因为它们本质上就是慢的,有其原因。唯一加快速度的方法是模拟那些慢速的外部资源。但是这些资源被专门测试以便发现单元测试可能遗漏的错误。例如,在我们的情况下,如果我们模拟输入流,第二个测试将无法发现错误。因此,第二个(集成)测试必须变慢才能有价值。

将测试分类为快速深入可能是一个解决方案。第一类包括尽可能模拟并在运行时不超过20毫秒的测试。第二类包括更深入地探测以发现可能被更快的测试忽略的难以捉摸的错误。往往情况下,单元测试属于第一类,而集成测试属于第二类。在我看来,“单元对集成”的区分是误导性的。“快速对深入”则更加清晰,因为可以明确地判断一个测试属于哪个类别。如果一个测试运行时间不超过20毫秒,则属于快速;否则,属于深入。

一旦测试被指定为快速或深度测试,它们应该在两种不同的场景中运行:程序员在编码过程中运行快速测试,而服务器在软件构建和/或发布阶段执行深度测试。在JUnit5中,可以使用@Tag注解实现这种分类。

@Test
@Tag("fast")
void readsSomeData() throws IOException {
  // ...
}
@Test
@Tag("deep")
void readsFromManyFiles(@TempDir Path tmp) throws IOException {
  // ...
}

在大多数情况下,明显的错误会被快速测试发现,这为程序员在编辑代码时带来信心。只有在罕见的情况下,快速测试无法发现某些错误时,深入测试才会捕捉到它们。只有在这种情况下,程序员才会在他们的笔记本电脑上运行慢速测试。

这是如何通过默认配置来打开“快速”测试的pom.xml文件。

<plugin>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
    <groups>fast</groups>
  </configuration>
</plugin>

在CI环境中,必须使用以下标志启动Maven:

$ mvn test -Dgroups=slow

程序员还可以在自己的笔记本电脑上使用相同的命令行标志运行“慢速”测试。然而,通常只有在服务器发出红色信号时才会这样做。

顺便提一下,toString() 方法还有一个错误,既不被第一个测试也不被第二个测试检测到。你能找出它吗?你能设计一个测试来揭示这个错误吗?你会将这个测试归类为“快速”还是“深入”?

Translated by ChatGPT gpt-3.5-turbo/35 on 2023-09-20 at 21:59

sixnines availability badge   GitHub stars