pytest 中的 Flaky 测试:识别根因、修复与缓解的完整指南
pytest 中的 Flaky 测试识别根因、修复与缓解的完整指南【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest本文基于 pytest 官方文档 flaky.rst 编写并结合当前仓库源码src/_pytest/与测试用例进行深度印证。内容覆盖 Flaky 测试的定义、典型根因、pytest 内置应对特性xfail strict、PYTEST_CURRENT_TEST、插件生态与通用治理策略帮助你诊断和驯服测试套件中的幽灵失败。Flaky 测试flaky test是指表现出间歇性、偶发性失败的测试有时通过有时失败且原因不明呈现出非确定性行为。在一个使用持续集成CI服务器的团队中Flaky 测试尤其令人头疼——CI 要求所有测试通过后才能合并代码变更而 Flaky 测试让测试结果不再是可靠的信号开发者无法确定测试失败究竟意味着代码变更破坏了测试还是仅仅是一次随机抖动。这会导致团队成员逐渐对测试结果失去信任甚至忽视真正的失败同时反复重跑测试套件、调查莫名失败也消耗大量时间。本文从根因出发系统梳理 pytest 生态中可用于识别、修复和缓解 Flaky 测试的手段。潜在根因分析系统状态依赖与隔离不足从广义上说一个 Flaky 测试的出现通常意味着测试依赖了某种未被恰当控制的系统状态——即测试环境没有做到足够隔离。越是高层的测试集成测试、端到端测试越容易 Flaky因为它们依赖的状态更多。Flaky 测试常常在测试套件并行运行时浮出水面例如使用pytest-xdist插件并行分发这往往暗示测试存在执行顺序依赖可能是另一个测试没有做好清理工作残留的数据导致当前测试失败可能是当前测试依赖了某个前序测试留下的数据而在并行运行时前序测试并不总是先执行凡是修改全局状态的测试通常都无法在并行环境下运行。过度严格的断言过于严苛的断言是 Flaky 测试的另一大来源典型场景集中在浮点数比较与时序问题上。此时pytest 提供了 pytest.approx 用于近似比较从而避免因极小的浮点误差或毫秒级时序抖动导致偶发失败。从源码看approx.py 为标量定义了默认容差DEFAULT_ABSOLUTE_TOLERANCE: float | Decimal 1e-12 DEFAULT_RELATIVE_TOLERANCE: float | Decimal 1e-6即默认采用 1e-6 的相对容差rel与 1e-12 的绝对容差abs并支持通过nan_ok参数决定 NaN 是否视为相等见 ApproxScalar.eq及 nan_ok 判断逻辑。实际使用中可根据业务精度显式指定容差assert result approx(3.14, rel1e-3) # 指定相对容差 assert result approx(3.14, abs0.01) # 指定绝对容差除标量外pytest.approx还覆盖 numpy 数组ApproxNumpy、映射/字典ApproxMapping、序列ApproxSequenceLike以及Decimal、timedelta等类型帮助在各类比较场景中规避过度严格的偶发失败。线程安全与 pytest 的执行模型需要澄清一个常见的误解pytest 本身是单线程的。它总是在同一个线程中顺序执行测试自身从不主动派生线程。即使是pytest-xdist这类并行插件通常也是通过派生多个进程、按批次运行测试来实现并行的而非使用多线程。当然测试和夹具完全可以在自己的测试流程中开线程例如某个 fixture 在后台启动一个服务器线程或被测生产代码本身就派生线程。此时需要注意两点务必在测试结束或 fixture 的 teardown 阶段最终等待join所有派生的线程否则可能出现测试主体已结束、后台线程仍在改写状态导致的竞态避免从多个线程中使用 pytest 提供的原语如 pytest.warns、pytest.raises 等它们并非线程安全。如果你的测试套件使用了线程且出现 Flaky 结果请不要排除这样一种可能测试正隐式地依赖了 pytest 内部的全局状态。pytest 内置的应对特性Xfail strict手动隔离区pytest.mark.xfail 标记配合strictFalse默认值可以让一个测试的失败不会导致整个构建失败。从语义上看这相当于为已知不稳定或有缺陷的测试建立了一个手动隔离区。需要强调的是这种用法长期使用是相当危险的被隔离的测试会持续静默失败问题可能被长期掩盖而无人修复。它更适合作为临时性的缓解手段。从 skipping.py 源码可以看清 strict 的完整语义。xfail 标记求值后得到Xfail数据类字段为raises、reason、run、strict见 skipping.py#L197-L211其中strict的取值链路如下见 evaluate_xfail_marks若标记中显式给出strict直接采用否则回退到strict_xfailini 配置项若仍未配置再回退到strictini 配置项。而 pytest.ini / pyproject.toml 中strict_xfail的默认值为 False并支持别名xfail_strict。在报告阶段见 pytest_runtest_makereport语义为测试确实失败符合 xfail 预期→ 输出为XFAILskipped测试意外通过XPASS且strictTrue→ 输出为failed长报告标记为[XPASS(strict)]测试意外通过且strictFalse→ 输出为XPASSpassed不破坏构建。这一行为在测试套件中有明确验证例如 testing/test_skipping.py 中的test_strict_xfail断言XPASS(strict)文本以及 testing/test_cacheprovider.py 中的test_xfail_strict_considered_failure验证 strict xfail 的意外通过被视为失败。PYTEST_CURRENT_TEST定位卡住的测试当测试会话卡死、而 pytest 是在安静模式-q下运行或无法访问控制台输出时很难判断到底哪个测试卡住了——尤其是这种问题只偶发出现正是典型的 Flaky 场景。此时可用 PYTEST_CURRENT_TEST 环境变量。pytest 在运行测试时会设置该环境变量其实现位于 runner.py 的_update_current_test_var将环境变量设置为当前测试的 nodeid 当前阶段阶段取值为setup、call、teardown测试结束teardown 完成后则从环境中删除该变量。同时出于环境变量的安全限制实现中会对值里的空字节做转义对应 issue #2644、#2957见 changelog.rst 相关记录。可以借助psutil之类的进程监控工具扫描所有进程的环境变量找出卡住的是哪个测试import psutil for pid in psutil.pids(): environ psutil.Process(pid).environ() if PYTEST_CURRENT_TEST in environ: print(fpytest process {pid} running: {environ[PYTEST_CURRENT_TEST]})例如运行foo_module.py中的test_foo时PYTEST_CURRENT_TEST会依次被设置为foo_module.py::test_foo (setup)foo_module.py::test_foo (call)foo_module.py::test_foo (teardown)顺序如上。需要留意的是官方文档 example/simple.rst 明确提示PYTEST_CURRENT_TEST的内容仅用于人类阅读其具体格式可能在版本间变化甚至 bug 修复也会改变因此不应依赖它做脚本化或自动化解析。第三方插件生态失败重跑类插件对失败的测试给予额外重试机会可以缓解 Flaky 测试对整体构建的负面影响让构建不因偶发失败而整体崩溃。当前文档提及的插件包括pytest-rerunfailures最常用的失败重跑插件可对失败用例配置重试次数与重试间隔pytest-replay帮助在本地复现 CI 运行中出现的崩溃或 Flaky 测试pytest-flakefinder来自 Dropbox 的工具用于反复运行测试以暴露偶发性失败。随机化插件有意打乱测试执行顺序的插件可以帮助暴露存在状态问题的测试pytest-random-order随机化测试执行顺序暴露顺序依赖pytest-randomly随机化顺序的同时随机化各种种子进一步增加发现状态耦合的概率。需要说明以上插件均通过pip install 插件名安装并在 pytest 配置中按插件自身文档启用本仓库只读请以各插件官方发布为准。其他通用策略拆分测试套件常见的做法是把一个庞大的测试套件拆成两个例如单元测试与集成测试两个部分并且只把单元测试套件作为 CI 准入门槛。这有助于控制构建时长高层测试通常更慢并避免偶发失败的集成测试频繁阻塞合并。但代价是破坏构建的代码变更有可能通过 CI 被合并因此需要对集成测试结果保持额外警惕密切监控。失败时保存截图/视频对于 UI 类测试失败时 UI 的实际状态至关重要。pytest-splinter 可以配合 pytest-bdd 等插件在测试失败时自动保存截图从而帮助定位失败原因、还原现场。删除或重写测试如果被测功能已被其他测试覆盖考虑直接删除该 Flaky 测试如果未被覆盖则考虑在更低的层级重写测试——底层测试依赖的状态更少往往能消除 Flaky 或让问题根源更加清晰。隔离Quarantine即把疑似 Flaky 的测试隔离起来如通过 xfail 或单独标记跳过等待修复后再回归测试套件。Mark Lapierre 在 2018 年的文章《Pros and Cons of Quarantined Tests》中系统讨论了隔离测试的利弊。CI 工具的失败自动重跑部分 CI 平台原生提供 Flaky 测试识别与失败重跑能力。例如 Azure Pipelines原 Visual Studio Team Services / VSTS就内置了识别 Flaky 测试并自动重跑失败测试的特性。进一步研究与参考资源相关研究文献当前文档列出了一份作者自述有限的研究清单涵盖 Flaky 测试的检测、根因与成本分析例如Gao 等人2015ICSE面向系统级用户交互测试的可重复性研究Palomba 与 Zaidman2017ICSME测试坏味道重构与 Flaky 测试修复的关系Bell 等人2018ICSEDeFlaker——自动检测 Flaky 测试Dutta 等人2020ISSTA在概率与机器学习应用中检测 Flaky 测试Habchi 等人2022ICSME定位引发测试 Flaky 的代码类Lamprou2022硕士论文测试顺序依赖与测试坏味道的实证研究Leinen 等人2023持续集成中 Flaky 测试成本的工业案例研究。文档同时邀请读者通过 issue 或 PR 扩充这份清单。优质资源文档还汇总了来自业界与学术界的实践资源可概括为几类Martin Fowler 2011 年的经典文章《Eradicating Non-Determinism in Tests》ThoughtWorks 关于 Go 团队消灭 Flaky 测试的实践Angie Jones 在 SeleniumConf Austin 2017 的演讲《The Build That Cried Broken》Test and Code 播客关于 Flaky Tests 的专题以及 MicrosoftVSTS/Azure DevOps 团队、Google、Dropbox、Uber 等公司公开的 Flaky 测试治理经验如 Google 的《Flaky Tests at Google and How We Mitigate Them》、Uber 的《Handling Flaky Unit Tests in Java》与《Flaky Tests Overhaul at Uber》等。小结应对 Flaky 测试没有银弹但可以遵循清晰的路径先通过PYTEST_CURRENT_TEST定位卡死的测试、用随机化插件暴露顺序依赖再回到根因状态隔离、断言精度、线程安全逐一修复必要时用xfail(strictFalse)或失败重跑插件做临时缓解同时配合套件拆分、失败截图与隔离策略建立长效机制。结合 pytest 源码中 runner.py、skipping.py、approx.py 的实现细节你可以更精确地理解每个特性在底层是如何运作的从而做出更可靠的判断。【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考