使用Gradle减少重复测试的原理阐释

175 阅读4分钟

在你的开发过程中,测试通常是运行时间最长的操作。不必要地运行它们是最大的时间浪费。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为不同类型的测试创建额外的任务,如功能、性能或随机测试。对于其中的每一个,考虑它何时需要重新运行,并将其作为任务的输入模型。

不要浪费你的时间。