停止重新运行你的测试

目录

介绍

测试通常是你开发过程中运行时间最长的操作。不必要地运行它们是最终的时间浪费。 Gradle 通过其 构建缓存增量构建 功能帮助你避免这种成本。它知道你的任何测试输入(例如你的代码、你的依赖项或系统属性)何时发生更改。如果一切保持不变,Gradle 将跳过测试运行,为你节省大量时间。

所以你可以想象当我看到像 StackOverflow 上的这样的代码片段时,我是多么绝望

tasks.withType(Test) {
    outputs.upToDateWhen { false }
}

让我们谈谈这意味着什么以及为什么这是一个坏主意。

沟通意图 #

上面的代码片段只是说“永远不要重用此测试的输出”。但是为什么?是因为有一些 Gradle 不知道的隐藏输入吗?还是因为测试产生随机输出?读者无法判断。

确定性测试不需要重新运行 #

“疯狂就是一遍又一遍地做同样的事情,并期待不同的结果”

- 不是阿尔伯特·爱因斯坦

你的绝大多数测试应该是确定性的,即给定相同的输入,它们应该产生相同的结果。如果不是这种情况,你的项目就麻烦大了。停止阅读这篇文章,开始修复你的代码!

重新运行确定性测试是在浪费你团队的时间。

非确定性测试 #

在某些情况下,你可能想要重新运行一些测试,即使没有任何代码更改。在这些情况下,你应该正确地建模额外的输入。告诉 Gradle 究竟是什么使你的测试成为非确定性的。

随机测试 #

一些测试使用随机化来提高你的软件质量。

  • 随机测试 可以用来确保生产代码可以处理各种输入,而不仅仅是开发人员想出的那些。
  • 变异测试 以细微的方式更改生产代码(例如,引入差一错误),并检查你的测试套件是否捕获了这些错误。

通过使随机种子成为你任务的输入,使这种随机化变得显式

task randomizedTest(type: Test) {
    systemProperty "random.testing.seed", new Random().nextInt()
}

这将迫使 Gradle 始终重新运行该测试,因为它将始终具有不同的种子。更好的是,你可以使种子用户可配置,以便在本地重现构建服务器上发现的错误。

系统集成测试 #

系统集成测试针对由其他团队控制的其他应用程序的真实版本验证你的应用程序。即使你没有更改任何内容,它们也可能失败,例如,因为另一个团队破坏了 API。它们也往往是你代码库中最慢的测试之一,因此你不希望仅仅因为你更改了一些文档而重新运行它们。一个好的折衷方案可能是至少每天检查一次集成,即使你这边没有任何更改。

使此间隔成为你的测试输入的一部分

task systemIntegrationTest(type: Test) {
    inputs.property "integration.date", LocalDate.now()
}

然后,你可以设置一个自动化构建,在每个人开始工作之前的早上运行此测试,并让它将结果推送到共享构建缓存。当你的团队来上班时,测试结果将从缓存中下载,并且当天不必重新运行测试。然后,他们可以将节省的时间用于更高效的任务,例如修复错误或开发新功能。

不稳定的测试 #

有时你会遇到一个错误,该错误仅使测试在 10 次中失败 1 次。为了分析情况,你希望 Gradle 重新运行测试,即使它之前已成功。在这种情况下,使用我上面展示的随机输入方法是合理的。但是,我发现将不稳定的测试包装在一个无限循环中更有效率。这样,我可以保持 IDE 中的调试器运行,甚至可以在运行时进行小的更改,而无需重新启动。

正确地建模你的测试 #

正确地建模你的需求对于你的构建逻辑和你的生产代码同样重要。正确地执行此操作将使你的构建更快,你的团队更高效。

java 插件的内置 test 任务旨在用于确定性和快速单元测试。将所有测试都放在这一个任务中并在每次构建时重新运行它们似乎很方便,因为其中一些测试是非确定性的。这种懒惰的方法将为你节省几分钟的思考和编码时间,但长期成本是巨大的,每天都会拖慢你团队中每个人的速度。

相反,为不同类型的测试(如功能测试、性能测试或随机测试)创建额外的 Test 任务。对于每个任务,请考虑何时需要重新运行它,并将其建模为任务的输入。

不要浪费你的时间。

讨论