# Gradle 不稳定测试 {#gradle-flaky-tests}

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

![不稳定测试页面](/images/guides/features/test-insights/flaky-tests-page.png)

## 不稳定检测的工作原理 {#how-it-works}

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

### 测试重试 {#test-retries}

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

你可以使用 [Test Retry 插件](https://github.com/gradle/test-retry-gradle-plugin) 或类似机制重新运行失败的测试。Tuist 将检测那些在某些尝试中通过而在其他尝试中失败的测试。

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

```kotlin
plugins {
    id("org.gradle.test-retry") version "1.6.2"
}

tasks.test {
    retry {
        maxRetries = 3
        maxFailures = 5
        failOnPassedAfterRetry = false
    }
}
```

![不稳定测试用例详情](/images/guides/features/test-insights/flaky-test-case-detail.png)

### 跨运行检测 {#cross-run-detection}

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

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

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

## 管理不稳定测试 {#managing-flaky-tests}

### 自动清除

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

### 手动管理

你还可以从测试用例详情页面手动将测试标记为不稳定或取消标记。这在以下情况下很有用：
- 你想在修复已知不稳定测试时予以确认
- 由于基础设施问题，测试被错误地标记

## 隔离不稳定测试 {#quarantining}

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

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

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

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

### 为什么被隔离的测试可能显示为通过 {#quarantined-passing}

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

### 启用隔离 {#enabling-quarantine}

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

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

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

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

## 对新测试进行压力测试 {#stress-testing-new-tests}

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

如果测试用例在过去 90 天内未在项目的默认分支上运行过 CI，或未在随后合并到该分支的提交上运行过，则视为新测试用例。使用合并队列时，队列运行的测试用例在队列的提交合并后即被知晓，如果队列丢弃了该测试用例，则其保持为新测试用例。提交通过 Tuist 在 CI 上收集的 <.localized_link href="/guides/features/test-insights/gradle#git-history">Git 历史</.localized_link> 进行匹配。

### 启用它

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

- **`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-notifications}

通过在 Slack 集成中设置 <.localized_link href="/guides/integrations/slack#flaky-test-alerts">不稳定测试警报</.localized_link>，在测试变得不稳定时立即收到通知。

## 查询不稳定和隔离状态 {#querying}

### CLI

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

```sh
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 参考中的 [测试用例端点](https://tuist.dev/api/docs#tag/test-cases)，以获取完整的路由、过滤器和响应字段列表。要更改测试用例的状态（标记或取消标记为不稳定、静音、跳过），请使用 `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 工具实现。
