pytest 缓存与跨会话状态管理指南:--lf/--ff/--nf 重跑失败用例、config.cache 对象与 stepwise 步进调试 📅 发布时间:2026/9/15 19:55:45 👁 浏览次数: pytest 缓存与跨会话状态管理指南--lf/--ff/--nf 重跑失败用例、config.cache 对象与 stepwise 步进调试【免费下载链接】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 内置cacheprovider插件的完整实战指南。它解决测试开发中最常见的两个痛点上次跑挂的用例如何快速重跑--lf/--ff/--nf以及如何在多次pytest调用之间持久化共享状态config.cache对象与cachefixture。读完本文你将掌握从失败重跑到缓存数据读写再到逐条步进修复的一整套跨会话工作流并能借助仓库源码理解这些功能底层的实现机制。插件总览默认启用、按需关闭缓存与重跑能力由一个名为cacheprovider的内置插件提供内部名为cacheprovider而非cache这是为了避免与早期的外部pytest-cache插件冲突见 cacheprovider.py 的注释。它默认启用且同时完成了两件彼此独立的事提供--lf/--ff/--nf等命令行选项用于根据上一次运行结果重排、重跑用例提供一个可在测试、conftest.py、插件之间共享的cache对象用于跨pytest调用持久化JSON 可序列化的值。如果确实不需要可以通过-p no:cacheprovider方式禁用它对应官方文档中的cmdunregister机制。禁用后config.cache属性将不再存在这是下文cachefixture 示例中先做getattr(pytestconfig, cache, None)判空的原因。核心用法重跑上次失败的用例插件为重跑上一次pytest调用中的失败用例提供了两个命令选项--lf, --last-failed只重跑上次失败的用例--ff, --failed-first先跑上次失败的用例再跑其余全部用例。这两个选项的注册位于 cacheprovider.py 的pytest_addoption钩子中。它们的实现核心是LFPlugin见 cacheprovider.py会话启动时从缓存读取cache/lastfailed键得到上次失败用例的 nodeid 集合在pytest_collection_modifyitems钩子中cacheprovider.py把items分为上次失败的与上次通过的两组--lf时仅保留前者并对其余做 deselected--ff时按失败在前、通过在后重排每次用例执行结束后通过pytest_runtest_logreport钩子cacheprovider.py更新lastfailed字典call阶段通过或跳过则移除记录失败则记录会话结束时pytest_sessionfinishcacheprovider.py把最新的lastfailed写回缓存供下一次调用读取。完整演示50 个用例中 2 个失败先用一个参数化用例构造出50 个用例、只有 2 个失败的场景# content of test_50.py import pytest pytest.mark.parametrize(i, range(50)) def test_num(i): if i in (17, 25): pytest.fail(bad luck)第一次运行不带任何选项会看到两个失败$ pytest -q .................F.......F........................ [100%] FAILURES _______________________________ test_num[17] _______________________________ i 17 pytest.mark.parametrize(i, range(50)) def test_num(i): if i in (17, 25): pytest.fail(bad luck) E Failed: bad luck test_50.py:7: Failed _______________________________ test_num[25] _______________________________ i 25 pytest.mark.parametrize(i, range(50)) def test_num(i): if i in (17, 25): pytest.fail(bad luck) E Failed: bad luck test_50.py:7: Failed short test summary info FAILED test_50.py::test_num[17] - Failed: bad luck FAILED test_50.py::test_num[25] - Failed: bad luck 2 failed, 48 passed in 0.12s紧接着用--lf运行只收集并执行了上次失败的 2 个用例其余 48 个用例被 deselected不执行$ pytest --lf test session starts platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 2 items run-last-failure: rerun previous 2 failures test_50.py FF [100%] FAILURES _______________________________ test_num[17] _______________________________ ... short test summary info FAILED test_50.py::test_num[17] - Failed: bad luck FAILED test_50.py::test_num[25] - Failed: bad luck 2 failed in 0.12s 注意输出中的run-last-failure: rerun previous 2 failures这一行——它来自LFPlugin.pytest_report_collectionfinishcacheprovider.py用于报告本次重跑的状态。改用--ff则所有 50 个用例都会执行但上次失败的 2 个排在最前面输出中先出现FF再出现 48 个点号$ pytest --ff test session starts platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 50 items run-last-failure: rerun previous 2 failures first test_50.py FF................................................ [100%] FAILURES _______________________________ test_num[17] _______________________________ ... 2 failed, 48 passed in 0.12s 这两种模式在 testing/test_cacheprovider.py 的test_lastfailed_usecase测试中被完整覆盖它先制造失败再验证--lf只跑失败用例、以及--cache-clear后缓存清空导致--lf退化为全量运行。--ff的排序行为则由test_failedfirst_ordertesting/test_cacheprovider.py验证——失败用例test_b必须出现在通过用例test_a之前。--nf / --new-first新文件优先除了失败重跑插件还提供了--nf, --new-first选项先运行来自新文件的用例再运行其余用例在两类内部都按文件修改时间mtime降序排列越新的文件越靠前。该功能的实现类是NFPlugincacheprovider.py。它的原理是读取缓存的cache/nodeids键得到上次运行记录过的全部用例 nodeid 集合在pytest_collection_modifyitems中把当前用例分成新出现的不在缓存中与旧有的两组分别按item.path.stat().st_mtime降序排序后拼接新文件组在前会话结束时把本次的 nodeid 集合写回cache/nodeids。从源码看--nf更适用于代码/用例不断增改、希望优先验证新改动的迭代场景它依赖文件系统 mtime 作为新旧依据_get_increasing_order见 cacheprovider.py因此对文件时间戳被外部工具改动的情况会比较敏感。上次没有失败时怎么办--lfnf--lfnf, --last-failed-no-failures选项专门控制--lf在没有任何已知失败或缓存里根本没有lastfailed数据时的行为可选值只有两个取值行为默认all没有已知失败时运行全部用例完整测试套件✅ 默认none没有已知失败时只打印一条说明消息并以成功状态退出不跑任何用例对应命令示例pytest --last-failed --last-failed-no-failures all # 跑全量默认行为 pytest --last-failed --last-failed-no-failures none # 不跑任何用例并成功退出该选项在 cacheprovider.py 中以choices(all, none)、defaultall注册。其运行逻辑位于LFPlugin.pytest_collection_modifyitems的 else 分支cacheprovider.py当lastfailed为空且选项为none时把所有 items deselected即一条消息、零执行、成功退出。这一行为在 testing/test_cacheprovider.py 中有两个专门测试test_lastfailed_no_failures_behavior_all_passed全部用例通过后--lf --lfnf none不再执行任何用例test_lastfailed_no_failures_behavior_empty_cache--cache-clear清空缓存lastfailed不存在后--lfnf none同样不执行用例。config.cache 对象跨会话持久化任意 JSON 数据--lf/--ff之所以能记住上次失败靠的正是插件提供的config.cache对象。第三方插件和conftest.py同样可以读写它把任意JSON 可编码的值在多次pytest调用之间持久化。Cache类的实现位于 cacheprovider.py它的两个核心方法是cache.get(key, default)读取键对应的缓存值缓存未命中或值损坏无法解析为 JSON时返回default见 cacheprovider.pycache.set(key, value)把值以 JSON 格式写入缓存cacheprovider.py支持嵌套的 list/dict 等基础 Python 类型组合。键的规范要求必须是/分隔的字符串且不能解析出缓存目录之外源码通过_join_within做词法归一化与包含性校验见 cacheprovider.py第一个路径段通常约定为你的插件或应用名避免与其他使用者冲突。缓存文件实际存储在缓存目录下的v/values由set()写入与d/directories由mkdir()创建两个子目录中见 cacheprovider.py。此外Cache还提供了cache.mkdir(name)方法cacheprovider.py返回一个缓存目录内的目录路径适合存放数据库转储等二进制大文件——注意name不能包含/分隔符。相关的键转义、mkdir边界校验等行为均有测试覆盖testing/test_cacheprovider.py。基于 cache fixture 的跨会话复用示例官方文档给出了一个经典示例插件用一个 fixture 复用上一次运行中耗费大量计算得到的状态。第一次运行会打印running expensive computation...第二次运行则直接从缓存读取、不再重复计算# content of test_caching.py import pytest def expensive_computation(): print(running expensive computation...) pytest.fixture def mydata(pytestconfig): cache getattr(pytestconfig, cache, None) if cache is None: # pytestconfig not having the cache attribute means the # cache plugin is disabled. expensive_computation() return 42 val cache.get(example/value, None) if val is None: expensive_computation() val 42 cache.set(example/value, val) return val def test_function(mydata): assert mydata 23第一次运行注意Captured stdout setup中的打印输出说明真的执行了昂贵的计算$ pytest -q F [100%] FAILURES ______________________________ test_function _______________________________ mydata 42 def test_function(mydata): assert mydata 23 E assert 42 23 test_caching.py:26: AssertionError -------------------------- Captured stdout setup --------------------------- running expensive computation... short test summary info FAILED test_caching.py::test_function - assert 42 23 1 failed in 0.12s第二次运行Captured stdout setup为空值来自缓存$ pytest -q F [100%] FAILURES ______________________________ test_function _______________________________ mydata 42 def test_function(mydata): assert mydata 23 E assert 42 23 test_caching.py:26: AssertionError short test summary info FAILED test_caching.py::test_function - assert 42 23 1 failed in 0.12s代码中的getattr(pytestconfig, cache, None)判空很重要它确保在-p no:cacheprovider禁用了缓存插件时fixture 仍能优雅回退到每次都重新计算的路径。在测试中直接注入 cache fixturecache本身也是一个可直接注入的 fixture定义见 cacheprovider.py。下面的测试验证了get/set的读写往返以及键不存在时必须提供 default否则抛TypeError的约定import pytest def test_cachefuncarg(cache): val cache.get(some/thing, None) assert val is None cache.set(some/thing, [1]) with pytest.raises(TypeError): cache.get(some/thing) val cache.get(some/thing, []) assert val [1]对应测试test_cachefuncarg位于 testing/test_cacheprovider.py。查看缓存内容--cache-show任何时候都可以用--cache-show直接查看缓存里存了什么。它会列出缓存目录位置、lastfailed/nodeids等内置键的值以及任何插件写入的自定义值$ pytest --cache-show test session starts platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y rootdir: /home/sweet/project cachedir: /home/sweet/project/.pytest_cache --------------------------- cache values for * --------------------------- cache/lastfailed contains: {test_caching.py::test_function: True} cache/nodeids contains: [test_caching.py::test_function] example/value contains: 42 no tests ran in 0.12s --cache-show还接受一个可选参数作为glob 过滤模式只显示匹配键的缓存项$ pytest --cache-show example/* test session starts platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y rootdir: /home/sweet/project cachedir: /home/sweet/project/.pytest_cache ----------------------- cache values for example/* ----------------------- example/value contains: 42 no tests ran in 0.12s 从实现看--cache-show会跳过用例收集与执行直接以wrap_session方式运行cacheshow()函数见 cacheprovider.py 与 cacheprovider.py它同时列出v/下的缓存值和d/下的缓存目录文件并对包含..的 glob 做越界防护对应测试test_cache_show_escaping_glob见 testing/test_cacheprovider.py。清空缓存--cache-clear在运行前清空全部缓存使用--cache-clearpytest --cache-clear官方文档特别建议CI 服务器上的调用应当加上--cache-clear因为在隔离性与正确性优先于速度的环境里绝不应该让上一次运行留下的缓存污染本次结果。实现上Cache.for_config在构造时就会检查该选项并调用Cache.clear_cachecacheprovider.py它只删除d/与v/两个子目录cacheprovider.py不会删除缓存目录本身及其中的README.md、.gitignore、CACHEDIR.TAG等支撑文件。缓存目录本身cache_dir 配置与自动生成的支持文件缓存默认存放在仓库根目录下的.pytest_cache/中。首次使用时pytest 会原子化地创建该目录并写入三份支撑文件见 cacheprovider.py 的CACHEDIR_FILESREADME.md说明该目录由cacheprovider插件维护并警告不要提交到版本控制.gitignore内容为*自动把所有缓存文件排除出版本控制CACHEDIR.TAG符合 bford.info/cachedir/spec 规范的缓存目录标记文件。目录位置可以通过 ini 配置cache_dir调整默认.pytest_cache定义见 cacheprovider.py[pytest] cache_dir .pytest_cache # 默认值相对路径基于 rootdir 解析也支持绝对路径与环境变量展开配置项的完整说明见 doc/en/reference/reference.rstcache_dir是字符串类型支持相对路径相对rootdir创建、绝对路径路径中还可以包含会被展开的环境变量若环境中设置了TOX_ENV_DIR默认值会自动变为$TOX_ENV_DIR/.pytest_cache见 cacheprovider.py。--cache-show的输出以及pytest -v的 session header 中都会显示实际使用的 cachedirpytest_report_header钩子见 cacheprovider.py。Stepwise逐条修复失败用例与--lf/-x不同的另一种调试思路是--sw, --stepwise它特别适合预期会有大量用例失败、想一次只修一个的场景。--stepwise的行为是测试套件运行到第一个失败用例处停止下一次调用时从上一次失败的那个用例继续运行直到遇到下一个失败再停止。如此反复你可以把失败用例一条一条地修完。它还提供了两个辅助选项--stepwise-skip跳过忽略当前这一个失败的用例把执行停止点推迟到第二个失败用例——当你被某个失败卡住、想先忽略它稍后再处理时很有用--stepwise-reset重置 stepwise 的缓存状态重新开始整个步进流程。提供--stepwise-skip或--stepwise-reset会隐式启用--stepwise见 stepwise.py 的pytest_configure。StepwisePlugin的实现位于 stepwise.py几个关键机制状态存于缓存键cache/stepwisestepwise.py包含last_failed上次失败用例的 nodeid、last_test_count上次 stepwise 运行时的用例总数用于检测套件变化后丢弃过期缓存、last_cache_date_str时间戳收集完成后pytest_collection_modifyitemsstepwise.py如果上次失败用例仍存在且测试总数未变就把它之前的所有用例 deselected并提示skipping N already passed items (cache from ... ago, use --sw-reset to discard)运行中遇到失败时pytest_runtest_logreportstepwise.py记录该失败并设置session.shouldstop立即中断整个会话配合--stepwise-skip时则把当前失败从记录中移除、不中断留到下一个失败再停会话结束时把最新状态写回缓存pytest_sessionfinishstepwise.py。注意 stepwise.py 与 cacheprovider.py 中都有针对 xdist worker 进程的保护检测到workerinput属性时不会写缓存避免多进程并发写导致的竞态对应 issue #10641。各选项速查表选项作用关键实现位置--lf, --last-failed只重跑上次失败的用例LFPlugincacheprovider.py--ff, --failed-first上次失败用例排在最前其余照常全跑同上--nf, --new-first新文件中的用例优先按 mtime 排序NFPlugincacheprovider.py--lfnf, --last-failed-no-failures无失败记录时--lf的行为all默认跑全量 /none不跑cacheprovider.py--cache-show [glob]显示缓存内容不做收集与执行cacheshow()cacheprovider.py--cache-clear运行前清空缓存CI 推荐Cache.clear_cachecacheprovider.py--sw, --stepwise跑到第一个失败停止下次从该失败继续StepwisePluginstepwise.py--sw-skip, --stepwise-skip忽略当前失败停在第二个失败隐式启用--swstepwise.py--sw-reset, --stepwise-reset重置 stepwise 状态隐式启用--swstepwise.py实践建议与注意事项日常迭代用--lf快速聚焦上次的失败修复后循环执行直至绿CI 中加--cache-clear避免缓存污染保证每次构建的隔离性与可复现性自定义缓存键加命名空间键的第一段用插件/应用名如myplugin/xxx防止与 pytest 内置的cache/lastfailed、cache/nodeids、cache/stepwise等键冲突只存 JSON 可序列化数据cache.set底层用json.dumpscacheprovider.py塞入不可序列化对象会抛TypeError测试test_config_cache_dataerror对此有覆盖见 testing/test_cacheprovider.py缓存读取要容错缓存文件可能被破坏或并发写入cache.get在解析失败时返回default插件代码应始终提供合理的默认值大批量失败修复用--stepwise配合--stepwise-skip跳过暂时不想处理的用例配合--stepwise-reset从头再来不要把.pytest_cache/提交进版本控制pytest 会自动在其中生成.gitignore但如果你使用自定义cache_dir请自行在仓库的.gitignore中排除对应路径。如果想深入源码验证建议从 cacheprovider.pyCache、LFPlugin、NFPlugin、stepwise.py 与 testing/test_cacheprovider.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),仅供参考