不健康的测试
不稳定的测试
什么是不稳定的测试?
这是一种有时会失败的测试,但如果你重试足够多次,最终它会通过。
测试不稳定可能的原因是什么?
状态泄露
标签: flaky-test::state leak
描述: 数据状态从之前的测试中泄露。实际原因可能不是这里的不稳定测试。
重现难度: 中等。通常运行相同的规范文件直到出现失败的测试就能重现问题。
解决方案: 修复之前的测试和/或修改测试数据或环境的地方,以便在每次测试后重置为干净的测试状态。
示例:
- 示例 1: 状态泄露可能由使用
let_it_be在测试示例间共享的数据记录导致,而某些测试会故意或无意地修改模型,导致测试示例中的数据不同步。这可能导致后续测试示例或重试中出现PG::QueryCanceled: ERROR。有关状态泄露和解决方案的更多信息,请参见 GitLab 测试最佳实践。 - 示例 2: 迁移测试可能回滚数据库,执行测试,然后在不一致的状态下恢复数据库,导致后续测试可能不知道某些列的存在。
- 示例 3: 一个测试修改了被后续测试使用的数据。
- 示例 4: 数据库查询测试在干净的数据库中通过,但在用于处理先前测试序列的 CI/CD 管道中失败。这可能意味着查询本身需要更新以在非干净数据库中工作。
- 示例 5: 异步请求中不相关的数据库连接被检查回来,导致测试意外使用这些不相关的数据库连接。此问题在此 合并请求 中得到解决。
- 示例 6: 数据库连接的最大生存时间导致这些连接断开,进而导致依赖这些连接事务的测试失败。此问题在此 合并请求 中得到修复。
- 示例 7: 测试中使用的 TCP 套接字在下一个测试前未关闭,而下一个测试也使用了相同端口的另一个 TCP 套接字。
- 示例 8:
let_it_be依赖于在before块中定义的存根。let_it_be在before(:all)期间执行,所以存根尚未设置。这使测试暴露在实际方法调用中,碰巧使用了方法缓存。
数据集特定
标签: flaky-test::dataset-specific
描述: 测试假设数据集处于特定(通常是有限的)状态或顺序,这取决于测试套件中测试运行的时机。
重现难度: 中等,因为重现问题所需的数据量可能难以在本地实现。通过多次运行测试更容易重现排序问题。
解决方案:
- 修复测试,使其不假设数据集处于特定状态,不要硬编码 ID。
- 如果测试不关心顺序而只关心元素,则放宽断言。
- 通过指定确定性顺序来修复测试。
- 通过指定确定性顺序来修复应用代码。
示例:
- 示例 1: 当任何表有超过 500 列时,数据库会被重新创建。它可能在合并请求中通过,但如果测试顺序改变,稍后在
master中可能会失败。 - 示例 2: 测试断言尝试查找具有不存在 ID 的记录会返回错误消息。测试使用硬编码的 ID,该 ID 应该不存在(例如
42)。如果测试在测试套件早期运行,它可能会通过,因为在此之前没有创建足够的记录,但一旦在套件中稍后运行,可能会有一个实际具有 ID42的记录,因此测试将开始失败。 - 示例 3: 如果不指定
ORDER BY,数据库不会提供确定性排序,或者测试中可能发生数据竞争。 - 示例 4。
SQL 查询过多
标签: flaky-test::too-many-sql-queries
描述: SQL 查询限制已达到,触发 Gitlab::QueryLimiting::Transaction::ThresholdExceededError。
重现难度: 中等,此失败可能取决于查询缓存的状态,而查询缓存可能受规范顺序的影响。
解决方案: 参见 查询计数限制文档。
随机输入
标签: flaky-test::random input
描述: 测试使用随机值,有时符合预期,有时不符合。
重现难度: 容易,因为测试可以在本地修改以使用测试失败时使用的"随机值"。
解决方案: 一旦问题重现,调试和修复测试或应用应该很容易。
示例:
- 示例 1: 测试不够健壮,无法处理特定数据,由于数据输入是随机的,这些数据只偶尔出现。
不可靠的 DOM 选择器
标签: flaky-test::unreliable dom selector
描述: 测试中使用的 DOM 选择器不可靠。
重现难度: 中等到困难。取决于 DOM 选择器是否重复,或是否在延迟后出现等。在 API 或控制器中添加延迟可能有助于重现问题。
解决方案: 这实际上取决于问题所在。可能是等待请求完成,向下滚动页面等。
示例:
- 示例 1: 匹配多个元素的非唯一 CSS 选择器,或不允许在抛出
element not found错误之前进行渲染时间的非等待选择器方法。 - 示例 2: CSS 选择器仅在 GraphQL 请求完成且 UI 更新后才出现。
- 示例 3: 误报测试,Capybara 在页面访问后立即返回 true,但页面未完全加载,或者元素无法被 webdriver 检测到(例如渲染在视口外或位于其他元素后面)。
日期时间敏感
标签: flaky-test::datetime-sensitive
描述: 测试假设特定的日期或时间。
重现难度: 容易到中等,取决于测试是否在特定日期后持续失败,或只在给定时间或日期失败。
解决方案: 冻结时间通常是一个好的解决方案。
示例:
不稳定的基础设施
标签: flaky-test::unstable infrastructure
描述: 测试由于基础设施问题而偶尔失败。
重现难度: 困难。重现 CI 基础设施问题真的很难。可能通过在本地使用容器实现。
解决方案: 通常在专门的问题中与基础设施部门开始对话是个好主意。
示例:
不正确的同步
标签: flaky-test::improper synchronization
描述: 不稳定测试问题源于时间相关因素,如延迟、最终一致性、异步操作或竞争条件。这些问题可能源于测试逻辑、被测系统或其交互的不足。虽然测试有时可以通过改进同步来解决这些问题,但它们也可能揭示需要解决的底层系统错误。
重现难度: 中等。例如,可以在功能测试中通过尝试引用尚未渲染的页面元素来重现,或在单元测试中通过未能等待异步操作完成来重现。
解决方案: 在端到端测试套件中,使用 eventually 匹配器。
示例:
如何本地重现不稳定的测试?
- 在本地重现失败
- 从 CI 作业日志中查找 RSpec
seed - 或运行
while :; do bin/rspec <spec> || break; done循环来查找seed
- 从 CI 作业日志中查找 RSpec
- 通过二分法减少示例,使用
bin/rspec --seed <之前找到的> --require ./config/initializers/macos.rb --bisect <spec>来规范失败 - 查看剩余的示例并注意状态泄露
- 例如,更新使用
let_it_be创建的记录是常见的问题来源
- 例如,更新使用
- 修复后,使用
seed重新运行规范 - 运行
scripts/rspec_check_order_dependence确保规范可以以 随机顺序 运行 - 再次运行
while :; do bin/rspec <spec> || break; done循环(顺便吃个午餐)以验证它不再不稳定
隔离的测试
当我们在 master 中有不稳定测试时:
- 创建一个带有相关组标签的 ~“failure::flaky-test” 问题。
- 在第一次失败后隔离测试。 如果测试无法及时修复,会影响所有开发人员的工作效率,因此应该将其隔离。
RSpec
快速隔离
除非你确实需要非常快地禁用测试(< 10分钟),否则考虑 使用 ~pipeline::expedited 标签代替。
要快速隔离测试而无需打开合并请求并等待流水线,你可以遵循 快速隔离流程。
始终继续 打开长期隔离合并请求 在快速隔离测试后!这是为了确保快速隔离的测试通过运行 CI/CD 流水线中的测试(不在快速隔离项目上下文中运行)得到正确修复。
长期隔离
一旦测试被快速隔离,你可以继续进行长期隔离过程。这可以通过打开合并请求来完成。
首先,确保测试文件有 feature_category 元数据,以确保正确归属测试文件。
然后,你可以使用 quarantine: '<issue url>' 元数据和之前创建的 ~“failure::flaky-test” 问题的 URL。
# 隔离单个规范
it 'succeeds', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345' do
expect(response).to have_gitlab_http_status(:ok)
end
# 隔离 describe/context 块
describe '#flaky-method', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345' do
[...]
end这意味着它将在 CI 中被跳过。默认情况下,隔离的测试将在本地运行。
我们也可以通过使用 --tag ~quarantine 运行来在本地开发中跳过它们:
# Bash
bin/rspec --tag ~quarantine
# ZSH
bin/rspec --tag \~quarantine同时确保:
- 合并请求上有 ~“quarantine” 标签。
- MR 描述中提到了不稳定测试问题,并使用了 将合并请求链接到问题的常用术语。
注意我们 不应该隔离共享示例/上下文,并且 我们不能隔离对 it_behaves_like 或 include_examples 的调用:
# 会被 Rubocop 标记
shared_examples 'loads all the users when opened', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345' do
[...]
end
# 不起作用
it_behaves_like 'a shared example', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345'
# 不起作用
include_examples 'a shared example', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345'长期隔离 MR 达到生产后,你应该 revert 之前创建的快速隔离 MR。
按功能类别查找隔离测试
要查找功能类别的所有隔离测试,使用 ripgrep:
rg -l --multiline -w "(?s)feature_category:\s+:global_search.+quarantine:"Jest
对于 Jest 规范,你可以使用 .skip 方法和 eslint-disable-next-line 注释来禁用 jest/no-disabled-tests ESLint 规则,并包含问题 URL。示例如下:
// quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/56789
// eslint-disable-next-line jest/no-disabled-tests
it.skip('should throw an error', () => {
expect(response).toThrowError(expected_error)
});这意味着除非使用 --runInBand Jest 命令行选项运行测试套件,否则它会被跳过:
jest --runInBand可以使用以下命令找到包含隔离规范的文件列表:
yarn jest:quarantine对于两个测试框架,确保向问题添加 ~"quarantined test" 标签。
一旦测试被隔离,有 3 个选择:
- 修复测试(即消除其不稳定性)。
- 将测试移动到更低的测试级别。
- 完全删除测试(例如,因为已经有更低级别的测试,或者它复制了同级别的另一个测试,或者它测试的内容过多等)。
自动重试和不稳定测试检测
在我们的 CI 中,我们使用 RSpec::Retry 自动重试失败的几次(精确的重试次数参见 spec/spec_helper.rb)。
我们还使用自定义的 Gitlab::RspecFlaky::Listener。
此监听器在 master 分支上的 maintenance 定期流水线的 update-tests-metadata 作业中运行,并将不稳定示例保存到 rspec/flaky/report-suite.json。
然后,所有流水线的 retrieve-tests-metadata 作业会检索该报告文件。
这最初在以下实现:https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13021。
如果你想在本地启用重试,可以使用 RETRIES 环境变量。例如 RETRIES=1 bin/rspec ... 会重试失败的示例一次。
要在本地生成报告,使用 FLAKY_RSPEC_GENERATE_REPORT 环境变量。例如 FLAKY_RSPEC_GENERATE_REPORT=1 bin/rspec ...。
rspec/flaky/report-suite.json 报告的使用
rspec/flaky/report-suite.json 报告被 导入到 Snowflake 中,用于通过 内部仪表板 进行监控。
我们在 GitLab 过去遇到的问题
rspec-retry在某些 API 规范失败时给我们带来麻烦: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/9825- 由于
PG::UniqueViolation导致的偶发性 RSpec 失败: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/9846 - ffaker 生成测试无法处理的数据(而且测试应该是可预测的,所以这很糟糕!):
- 使
spec/mailers/notify_spec.rb更健壮: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10015 - 在
spec/requests/api/commits_spec.rb中出现瞬时失败: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/9944 - 用序列替换 ffaker 工厂数据: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10184
- 在 spec/finders/issues_finder_spec.rb 中出现瞬时失败: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10404
- 使
依赖顺序的不稳定测试
要识别单个文件中的排序问题,阅读 如何本地重现不稳定测试。
一些不稳定测试可能根据它们与其他测试运行的顺序而失败。例如:
要识别不同文件间的排序问题,你可以使用 scripts/rspec_bisect_flaky,它会给我们提供重现失败的最小测试组合:
-
首先获取在不稳定测试之前运行的规范列表。你可以在 CI 作业输出日志中的
Knapsack node specs:下搜索。 -
将规范列表保存为文件,然后运行:
cat knapsack_specs.txt | xargs scripts/rspec_bisect_flaky
如果存在顺序依赖问题,上面的脚本将打印最小重现。
时间敏感的不稳定测试
- https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10046
- https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10306
数组顺序期望
功能测试
- 确保在开始练习前创建测试所需的所有数据: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12059
- Bis: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12604
- Bis: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12664
- 对底层数据库状态而不是页面内容进行断言: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10934
- 在 JS 测试中,移动元素可能导致 Capybara 在元素移动的准确时间点击时误点
- 在事件处理器设置前触发 JS 事件
- 在断言 Markdown 图片的
src属性时等待图片延迟加载 - 避免对 flash notice 横幅进行断言
Capybara 视口大小相关问题
- spec/features/issues/filtered_search/filter_issues_spec.rb 的瞬时失败: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10411
Capybara JS 驱动相关问题
- 在没有 AJAX 请求时不要等待 AJAX: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10454
- Bis: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12626
Capybara 期望超时
挂起的测试
如果测试挂起或在 CI 中超时,可能是由 Rails 中的 LoadInterlockAwareMonitor 死锁错误 导致。
要诊断,你可以使用 sigdump 打印 Ruby 线程转储:
-
在本地运行挂起的测试。
-
通过运行以下命令触发 Ruby 线程转储:
kill -CONT <pid> -
线程转储将保存到
/tmp/sigdump-<pid>.log文件中。
如果你看到带有 load_interlock_aware_monitor.rb 的行,这很可能相关:
/builds/gitlab-org/gitlab/vendor/ruby/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/concurrency/load_interlock_aware_monitor.rb:17:in `mon_enter'
/builds/gitlab-org/gitlab/vendor/ruby/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/concurrency/load_interlock_aware_monitor.rb:22:in `block in synchronize'
/builds/gitlab-org/gitlab/vendor/ruby/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `handle_interrupt'
/builds/gitlab-org/gitlab/vendor/ruby/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `synchronize'请参阅我们通过在发出请求前创建工厂来解决的示例:
- https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81112
- https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158890
- https://gitlab.com/gitlab-org/gitlab/-/issues/337039
建议
拆分测试文件
将大型 RSpec 文件拆分为多个文件可能有助于缩小上下文范围并识别有问题的测试。
通过强制作业运行相同的测试文件集来重现 CI 中的作业失败
在 CI 中重现作业失败总是有助于诊断测试失败的原因和方式。这需要我们以相同的规范顺序运行相同的测试文件。由于我们使用 Knapsack 在并行作业间分配测试,文件可能在两个流水线间分配不同,我们可以通过以下步骤硬编码此作业分配:
- 找到你想要重现的作业,识别它运行的提交,将你的本地
gitlab-org/gitlab分支设置为相同的提交,以确保我们运行相同的项目副本。 - 在作业日志中,找到 Knapsack 分配的规范文件列表 - 你可以搜索
Running command: bundle exec rspec,此命令的最后一个参数应包含文件名列表。复制此列表。 - 转到
tooling/lib/tooling/parallel_rspec_runner.rb,这是测试文件分配发生的地方。查看 此合并请求 作为示例,将从步骤 2 复制的文件列表存储在TEST_FILES常量中,并通过像示例 MR 中那样更新rspec_command方法让 RSpec 运行此列表。 - 跳过
spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb中的测试,以免你的流水线早期失败。 - 由于我们希望强制流水线针对特定版本运行,我们不想运行合并结果流水线。我们可以通过在 MR 中引入合并冲突来实现这一点。
- 为了保留规范顺序,通过使用原始失败作业中显示的值硬编码
Kernel.srand来更新spec/support/rspec_order.rb文件,如 此处 所示。你可以在作业日志中通过搜索Randomized with seed找到 srand 值,后跟此值。
指标与跟踪
- (Snowflake) 不稳定测试仪表板 (内部)
- (Snowflake) 不健康测试仪表板 (内部)
- (GitLab) GitLab.org 组不稳定测试问题看板
- (GitLab) “最不稳定测试"问题看板
- (Grafana) 端到端测试不稳定仪表板 (内部)
- (Tableau) 不稳定测试问题 (内部)
资源
慢速测试
最慢的测试
我们在 rspec_profiling_stats 项目中收集测试持续时间信息。数据通过 GitLab Pages 显示在此 UI 中。
在此 问题 中,我们定义了可作为指南的测试持续时间阈值。
对于超过阈值的测试,我们在 测试问题 中自动报告慢速发生情况,以便团队可以改进它们。
对于有正当原因且允许变慢的测试,添加 allowed_to_be_slow: true。
| 日期 | 功能测试 | 控制器和请求测试 | 单元测试 | 其他 | 方法 |
|---|---|---|---|---|---|
| 2023-02-15 | 67.42 秒 | 44.66 秒 | - | 76.86 秒 | 消除最大值的最慢测试 |
| 2023-06-15 | 50.13 秒 | 19.20 秒 | 27.12 | 45.40 秒 | 前 100 个最慢测试的平均值 |
处理不稳定或慢速测试的问题
围绕这些问题非常轻量级。随时可以关闭或不关闭它们,它们是 自动管理的:
- 如果不稳定或慢速测试被修复且关联的
[Test]问题未手动关闭,它将在 30 天不活动后 自动关闭。 - 如果问题再次发生,关闭的问题会自动重新打开。这意味着,当你认为已修复时,关闭问题也是可以的。