停止重新运行测试

测试通常是开发过程中运行时间最长的操作。不必要地运行它们是最大的时间浪费。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 任务。对于每个任务,考虑它需要何时重新运行,并将此作为任务的输入进行建模。

不要浪费时间。

讨论