跳到内容

Gradle 不稳定测试

不稳定测试是指在使用相同代码多次运行时产生不同结果(通过或失败)的测试。它们会削弱你对测试套件的信任,并浪费开发人员排查误报失败的时间。Tuist 会自动检测不稳定测试,并帮助你随时间跟踪它们。

不稳定测试页面

不稳定检测的工作原理#

Tuist 通过两种方式检测不稳定测试:

测试重试#

当你使用重试功能运行测试时,Tuist 会分析每次尝试的结果。如果某个测试在某些尝试中失败而在其他尝试中通过,它会被标记为不稳定。

你可以使用 Test Retry 插件 或类似机制重新运行失败的测试。Tuist 将检测那些在某些尝试中通过而在其他尝试中失败的测试。

将该插件添加到你的 build.gradle.kts:

kotlin
plugins {
id("org.gradle.test-retry") version "1.6.2"
}
tasks.test {
retry {
maxRetries = 3
maxFailures = 5
failOnPassedAfterRetry = false
}
}

不稳定测试用例详情

跨运行检测#

即使没有测试重试,Tuist 也可以通过比较同一提交和同一 Gradle 项目在不同 CI 运行中的结果来检测不稳定测试。如果某个测试在一次 CI 运行中通过,但在同一提交和项目的另一次运行中失败,则这两次运行都会被标记为不稳定。

Gradle 项目是比较键的一部分,因为同一个测试在不同项目中可能表现出确定性的差异——例如,一个共享的测试类使用不同的依赖项编译,或在不同的模块夹具上运行。同一提交在不同 Gradle 项目上的两次运行被视为独立的执行变体,不会互相标记为不稳定。

这对于捕获那些未能一致失败从而无法被重试捕获、但仍会导致间歇性 CI 失败的不稳定测试特别有用。

管理不稳定测试#

自动清除#

检测和清除通过项目上的 自动化警报 运行。每个项目都会获得一个默认的“不稳定测试检测”自动化,其 触发器 会将测试标记为不稳定,而其 恢复 会在测试经过配置的恢复窗口(默认 14 天)且未重新触发后清除该标记。在 设置 → 自动化 下编辑它以更改恢复窗口、更换恢复操作(例如也解除隔离),或完全禁用恢复,以便测试保持标记为不稳定,直到你手动清除它们。

手动管理#

你还可以从测试用例详情页面手动将测试标记为不稳定或取消标记。这在以下情况下很有用:

  • 你想在修复已知不稳定测试时予以确认
  • 由于基础设施问题,测试被错误地标记

隔离不稳定测试#

隔离会将不稳定测试孤立起来,使其在你修复时不会阻塞 CI。默认情况下,这是一个 手动操作——你可以从仪表板中的测试用例详情页面进行隔离、解除隔离和切换模式——但你也可以将其连接到 设置 → 自动化 下的 自动化警报,这样当测试超过不稳定性阈值时会自动静音(或跳过),并在恢复时解除隔离。每次转换,无论是手动还是自动,都会记录在测试用例的审计日志中。

默认情况下,恢复会等待直到触发条件不再成立,因此测试会保持隔离状态,直到其旧的不稳定运行离开触发窗口。为了更快恢复,将 恢复条件 设置为 无新的不稳定运行:一旦恢复窗口(天数或运行次数)过去且没有新的不稳定运行,测试就会恢复;每次新的不稳定运行都会重新开始等待;恢复后,只有新的不稳定运行才会再次将其隔离。

被隔离的测试处于以下两种模式之一:

  • 静音:测试仍然运行,但插件设置 ignoreFailures = true,并且仅在非隔离失败时使构建重新失败。失败仍然会提供给不稳定测试检测器,因此你可以在不破坏构建的情况下继续观察测试。选择此模式用于你正在积极调查的测试。
  • 跳过:插件通过 Gradle 的 excludeTestsMatching 过滤掉该测试,因此它永远不会启动。它不会产生新结果,并从不稳定测试仪表板上消失,直到你重新启用它。当测试损坏、缓慢或不稳定性如此持久以至于运行它只是浪费 CI 分钟数时,选择此模式。

为什么被隔离的测试可能显示为通过#

  • 静音测试 仍然执行。失败的静音测试在测试用例运行中被记录为 失败 并标记为不稳定——单个测试的状态不会被重写。被覆盖的是 整体测试运行:如果运行中每个失败的测试用例都被静音,则整个运行报告为通过,因此静音失败不会破坏 CI。
  • 跳过的测试 根本不会运行,因此仪表板继续显示测试最后一次实际执行的状态——该快照可能已有数周之久。

启用隔离#

隔离在 CI 上 自动启用(当设置 CI 环境变量时),并在本地构建中禁用。插件在每个测试任务之前从 Tuist 服务器获取隔离测试列表;静音测试正常运行并屏蔽其失败,跳过的测试被过滤掉。

你可以在你的 settings.gradle.kts 中明确控制此项:

kotlin
tuist {
testQuarantine {
enabled = true // or false to disable
}
}

当未设置 enabled 时,默认为自动检测:在 CI 上启用,在本地禁用。

对新测试进行压力测试#

压力门控会重新运行你的分支添加的测试用例,每个用例在全新进程中运行多次,并报告那些结果不稳定的用例。你会在引入新不稳定测试的拉取请求上看到它,而不是几周后。

如果测试用例在过去 90 天内未在项目的默认分支上运行过 CI,或未在随后合并到该分支的提交上运行过,则视为新测试用例。使用合并队列时,队列运行的测试用例在队列的提交合并后即被知晓,如果队列丢弃了该测试用例,则其保持为新测试用例。提交通过 Tuist 在 CI 上收集的 Git 历史 进行匹配。

启用它#

门控默认关闭。设置一种模式:

  • report:警告每个不稳定测试用例。运行仍按其自身结果退出。
  • enforce:不稳定测试用例会令运行失败,退出码与失败的测试相同。

先使用 report 几周,看看 enforce 会阻止哪些情况。

在 settings.gradle.kts 中声明它,或设置 TUIST_TEST_STRESS_NEW_TESTS 以按 CI 流水线调整模式:

kotlin
tuist {
stressNewTests {
mode = "report" // or "enforce"
}
}

与隔离不同,门控没有感知 CI 的默认值。未设置 mode 时,它不会在任何地方运行。

每个测试重新运行的次数#

这取决于测试用例在首次运行中所花费的时间:

首次运行重跑次数
最多 5 秒10
最多 10 秒5
最多 30 秒3
最多 5 分钟2
超过 5 分钟不重跑

每次重跑都是一个针对该单一测试用例过滤的嵌套 Gradle 构建,因此每次重跑都会获得一个全新的测试 JVM。当达到 200 个测试用例或 10 分钟时(以先到达者为准),过程停止,并说明触发了哪个限制。

门控不运行的情况#

  • 首次运行已经失败。请先修复这些测试。
  • 项目没有默认分支,或者尚未在其上运行过任何 CI。
  • 超过 30% 的项目测试用例看起来是新的。重跑这么多用例会得不偿失。在刚刚开始向 Tuist 报告的项目上,或在一次重命名移动了大量测试之后,可能会出现这种情况。

Slack 通知#

通过在 Slack 集成中设置 不稳定测试警报,在测试变得不稳定时立即收到通知。

查询不稳定和隔离状态#

CLI#

tuist test case 命令树展示了 Tuist 跟踪的关于测试用例的所有信息:

bash
tuist test case list --flaky # only flaky test cases
tuist test case list --quarantined # only muted or skipped test cases
tuist test case show <test_case_id> # detail: flakiness rate, last status, run counts
tuist test case events <test_case_id> # audit log: marked_flaky, muted, skipped, ...
tuist test case run list <test_case_id> # run history with status and duration
tuist test case run show <test_case_run_id> # single run, including failure breakdown

所有这些命令都接受 --json 以便进行脚本编写。

REST API#

相同的数据可通过 HTTP 获取——请参阅 API 参考中的 测试用例端点,以获取完整的路由、过滤器和响应字段列表。要更改测试用例的状态(标记或取消标记为不稳定、静音、跳过),请使用 is_flaky 或 state 发送 PATCH /api/projects/{account_handle}/{project_handle}/tests/test-cases/{test_case_id}。相同的更改也可通过 tuist test case update 以及 update_test_case Model Context Protocol 工具实现。