pytest 历史演进笔记解读从 Marker 重构、字符串条件到配置与缓存机制的兼容性指南【免费下载链接】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 官方仓库的 doc/en/historical-notes.rst 为骨架系统梳理 pytest 各版本演进过程中被替代或调整的旧特性包括 3.6 版 Marker 机制的彻底重构、缓存插件并入核心、pytest_funcarg__前缀与yield_fixture的退场、[pytest]配置节名的变更、parametrize旧式标记语法、字符串 skipif/xfail 条件的求值原理等。读者读完本文将掌握如何将旧式 Marker 访问代码迁移到iter_markers/get_closest_marker新 API、如何理解字符串条件求值时的命名空间构造、以及如何识别并规避已被弃用的历史 API。本文定位与阅读前提historical-notes.rst是 pytest 官方为查看旧代码的用户保留的历史档案记录了曾经存在、如今已变化的行为。它不等同于推荐用法而是兼容性地图当你面对遗留代码库、老教程或第三方插件中出现的get_marker、yield_fixture、pytest.set_trace()等写法时本文能帮你准确判断它们的语义、弃用状态与迁移方向。写作时以当前仓库包含 src/_pytest 源码与 testing 测试为事实依据所有结论均可回溯到对应源码文件验证。一、Marker 机制重构pytest 3.6 起从MarkerInfo到iter_markers1.1 旧设计的三大缺陷versionchanged:: 3.6是本文档的第一处关键变更标记。3.6 之前pytest 的 Marker 实现方式是直接往函数的__dict__里累加写入属性由此衍生出一系列设计缺陷跨类层级意外传播因为标记写进了函数对象的__dict__标记会以难以预期的方式沿类继承层级传递出现兄弟类互相染色的诡异现象。获取 API 不统一三种来源的标记存储形态各异——来自pytest.mark装饰器的标记是MarkerInfo内部对象可能还混入兄弟类的标记来自参数化parametrize的标记是MarkDecorators通过node.add_marker添加的标记会覆盖丢弃之前的标记而MarkerInfo表面上像一个单独的 mark实际却是多个同名 mark 的合并视图。模块/类/函数访问不一致标记即使声明在类或模块上也只能在函数上被访问到。这些问题的组合让高级用户几乎不可能在不深挖内部实现的前提下正确使用 Marker 数据进而引发各种隐蔽 bug。重构前这些问题在 issue 系统中积累了大量的真实案例详见下文相关问题清单。1.2 新 APIiter_markers与get_closest_markerpytest 3.6 起引入统一的新 API 并重写了内部实现核心是 src/_pytest/nodes.py 中Node基类上的两个方法def iter_markers(self, name: str | None None) - Iterator[Mark]: Iterate over all markers of the node. def get_closest_marker( self, name: str, default: Mark | MarkDecorator | None None ) - Mark | None: Return the first marker matching the name, from closest (for example function) to farther level (for example module level).从源码可以看到二者的实现基础iter_markers委托给iter_markers_with_node后者沿iter_parents()从节点自身向上直到收集树根8.1 版本新增该方法遍历每个父节点并逐一遍历node.own_markers实现收集整条继承链上的所有同名标记get_closest_marker则调用next(self.iter_markers(namename), default)——即取最近层级函数优先于类类优先于模块上第一个命中的标记若找不到返回default既可以是普通值也可以是MarkDecorator源码中会自动解包成其内部Mark。1.3 旧代码迁移指南两种场景旧 APINode.get_marker(name)之所以被弃用是因为它返回内部的MarkerInfo合并对象包含所有同名标记合并后的 name、*args和**kwargs语义暧昧且与真实存储不一致。文档给出了按语义分场景的两步迁移法场景一标记互相覆盖以最近者为准——比如模块级log_level(info)被某条测试函数级log_level(debug)覆盖你只关心当前生效的那一个# 替换前 marker item.get_marker(log_level) if marker: level marker.args[0] # 替换后 marker item.get_closest_marker(log_level) if marker: level marker.args[0]场景二标记叠加组合全部都要——比如skipif(condition)多个条件应全部参与求值顺序无关应当把它们当作一个集合# 替换前 skipif item.get_marker(skipif) if skipif: for condition in skipif.args: # eval condition ... # 替换后 for skipif in item.iter_markers(skipif): condition skipif.args[0] # eval condition这一迁移不仅是 API 换名更是语义从合并视图到原始对象的纠正。在当前的 src/_pytest/skipping.py 中evaluate_condition正是通过item.iter_markers(skipif)逐个取条件并求值可以视为新 API 在核心功能中的直接使用范例。1.4 参数化标记与add_marker的现代形态文档提到旧版参数化产生的标记是MarkDecorator且node.add_marker会覆盖丢弃先前标记。当前源码中两者都已被理顺参数化标记现代写法使用pytest.param(..., marks...)见 src/_pytest/mark/init.py 的param()工厂函数与 src/_pytest/mark/structures.py 的ParameterSet。每个参数集可携带MarkDecorator | Collection[MarkDecorator | Mark]从而与装饰器标记统一为同一种存储形态ParameterSet.marks不再产生旧版MarkDecorator与MarkerInfo两套体系并存的问题。node.add_marker在 src/_pytest/nodes.py 中接受字符串会经MARK_GEN解析为对应 mark或MarkDecorator并通过append参数控制是追加到own_markers末尾还是插入头部不再存在丢弃先前标记的副作用。1.5 新实现修复的相关 issue 清单非穷举文档列出重构所修复的历史问题可作为阅读旧 issue 与理解动机的索引Marks dont pick up nested classes#199Markers stain on all related classes#568Combining marks - args and kwargs calculation#2897request.node.get_marker(name)对类上应用的标记返回None#902参数化中应用的标记被存储为 markdecorator#2400向后不兼容地修复 marker 交互#1670重构 marks 以摆脱 marks transfer 机制#2363引入 FunctionDefinition 节点并在 generate_tests 中使用#2522移除命名 marker 属性并在 items 中收集 markers#891参数化产生的 skipif 标记隐藏模块级 skipif#1540skipif parametrize 不跳过测试#1296marker 传递与继承不兼容#535更多细节可查看原 PR文档中标注为 #3317。注意文档同时预告未来某个 pytest 主版本将引入基于类的 markersclass based markers届时 markers 将不再局限于pytest.Mark实例。二、缓存插件并入核心pytest-cache→ 内置 cache文档说明核心缓存插件的功能此前以第三方插件pytest-cache分发并入核心后命令行选项与 API 用法保持兼容唯一的硬性限制是——只能在测试运行之间存取 JSON 可序列化的数据。当前实现位于 src/_pytest/cacheprovider.py。源码印证了文档的两个关键点JSON 序列化约束get方法通过json.load(f)读取src/_pytest/cacheprovider.pyset方法通过json.dumps(value, ensure_asciiFalse, indent2)写入src/_pytest/cacheprovider.py。因此写入自定义对象前需要自行转换为 JSON 兼容结构。命令行选项pytest_addoption中注册--lf/--last-failed只重跑上次失败的测试--ff/--failed-first全部运行但失败者优先--nf/--new-first新文件优先--cache-show显示缓存内容可选 glob 参数默认*不执行收集与测试--cache-clear测试运行开始时清除全部缓存另有 ini 项cache_dir默认.pytest_cache若环境变量TOX_ENV_DIR存在则默认改为$TOX_ENV_DIR/.pytest_cache。在测试中通过request.config.cache或cachefixture访问缓存文档要求 key 使用/分隔的字符串且首段通常为插件名以避免冲突——这一约定在 src/_pytest/cacheprovider.py 的 cache fixture 文档字符串中被再次确认。三、fixture 演进史pytest_funcarg__、yield_fixture与 autouse3.1 2.3 之前的魔法前缀pytest_funcarg__在 2.3 版本之前没有pytest.fixture装饰器声明 fixture 工厂函数必须使用魔法前缀pytest_funcarg__NAME。文档明确表示这一旧语法至今仍受支持但已不再是声明 fixture 的主要推荐方式。也就是说遇到遗留代码中的def pytest_funcarg__tmpdir(request): return ...可以放心保留运行但新代码应迁移到pytest.fixture。3.2 2.10 起yield_fixture不再必要2.10 之前要用yield编写 teardown 代码必须给 fixture 打上yield_fixture标记2.10 之后普通 fixture 可直接yield该装饰器被弃用。当前仓库中 src/_pytest/fixtures.py 依然保留着yield_fixture的兼容实现并带有明确的弃用提示deprecated( pytest.yield_fixture is deprecated. Use pytest.fixture instead; they are the same., categoryNone, # We have our own runtime warning logic ) def yield_fixture(...):同时 src/_pytest/fixtures.py 中_teardown_yield_fixture的实现表明现代 fixture 的 yield 化 teardown 已成为一等公民yield之后的部分通过request.addfinalizer注册执行。3.3pytest.setup演变为 autouse fixture开发期曾短暂使用pytest.setup名称但在 2.3 发布前被重命名并融入通用 fixture 机制即autouse fixtures自动应用的 fixture无需显式请求。这是 pytest 中隐式 setup概念的最终形态。四、配置节名迁移[pytest]→[tool:pytest]3.0 之前setup.cfg中支持的节名是[pytest]。由于该名称可能与某些 distutils 命令冲突推荐节名改为[tool:pytest]。注意区分setup.cfg使用[tool:pytest]pytest.ini与tox.ini中节名仍然是[pytest]。这一点在 doc/en/example/customdirectory/pytest.ini 等示例配置中可以得到印证。五、parametrize的历史写法两则5.1 给参数值打标记旧式内联语法3.1 之前3.1 之前对参数值应用标记的机制是直接把pytest.mark.xfail(...)包在参数元组外层import pytest pytest.mark.parametrize( test_input,expected, [(35, 8), (24, 6), pytest.mark.xfail((6*9, 42))] ) def test_eval(test_input, expected): assert eval(test_input) expected文档明确指出这只是一次初始 hack它无法传入函数也无法对同名不同参数的多个标记正确应用因此计划在 pytest 4.0 移除。现代等价写法是 src/_pytest/mark/init.py 的pytest.parampytest.mark.parametrize( test_input,expected, [ (35, 8), pytest.param(6*9, 42, markspytest.mark.xfail), ], ) def test_eval(test_input, expected): assert eval(test_input) expectedpytest.param还支持id参数8.4 起可用pytest.HIDDEN_PARAM隐藏该参数集在测试名中的显示见 src/_pytest/mark/structures.py。5.2 参数名元组写法2.4 之前2.4 之前argnames必须写成元组pytest.mark.parametrize((a, b), [(1, 2), (3, 4)])该写法至今有效但逗号分隔字符串a,b更简洁、行噪音更少文档推荐优先使用字符串形式。六、skipif/xfail 条件字符串求值的历史与机制6.1 字符串条件及其求值命名空间2.4 之前skipif/xfail 条件只能用字符串书写import sys pytest.mark.skipif(sys.version_info (3,3)) def test_function(): ...求值发生在测试函数 setup 阶段等价于eval(sys.version_info (3,0), namespace)。命名空间按如下规则构造初始放入sys、os模块和 pytest 的config对象再用该测试函数的模块 globals更新。当前源码在 src/_pytest/skipping.py 的evaluate_condition中完全对应了这一描述字符串条件会被编译后evalglobals_字典中注入os、sys、platform与item.obj.__globals__语法错误会得到精心格式化的报错信息。由于config对象在命名空间中可用旧代码可以这样按配置项跳过pytest.mark.skipif(not config.getvalue(db)) def test_function(): ...6.2 为何推荐布尔条件 reason2.4 起官方推荐布尔条件理由是标记可以自由地在测试模块间导入字符串条件要求导入的不只是标记本身还包括条件里用到的全部变量破坏了封装性。布尔条件的现代写法pytest.fixture(autouseTrue) def skip_if_no_db(request): if not request.config.getoption(--db, defaultFalse): pytest.skip(--db was not specified) def test_function(): pass注意文档末尾附带了重要更正——pytest.config全局对象已在 pytest 5.0 移除应改用request.config通过requestfixture或pytestconfigfixture。字符串条件则将保持完全支持若无需跨模块导入标记可以继续使用。七、调试 APIpytest.set_trace()的退场2.4 之前设置断点需要使用pytest.set_trace()import pytest def test_function(): ... pytest.set_trace() # invoke PDB debugger and tracing如今不再需要直接使用原生import pdb; pdb.set_trace()即可。pytest 对pdb.set_trace()有内建的捕捉与交互增强详见文档中指引的 breakpoints 章节这也是移除多余包装、回归 Python 原生能力的一个典型演进。八、compat 兼容属性从Node上访问类对象的弃用通过Node实例访问Module、Function、Class、Instance、File、Item等对象早已被文档标记为弃用并从pytest 3.9 起开始发出警告。正确的做法是直接import pytest然后通过pytest模块访问这些对象如pytest.Function、pytest.Class。这也是所有历史 API 的通用迁移方向以顶层pytest命名空间为稳定出口。九、迁移检查清单基于全文给出一份可直接对照执行的检查清单旧写法状态迁移目标node.get_marker(name)已弃用node.get_closest_marker(name)覆盖语义或node.iter_markers(name)叠加语义pytest.mark.parametrize(...)内联pytest.mark.xfail(...)包裹参数已计划移除4.0pytest.param(..., marks...)pytest_funcarg__NAME前缀仍支持不推荐pytest.fixturepytest.yield_fixture已弃用pytest.fixture 直接yieldpytest.setup已移除2.3 前改名autouse fixturesetup.cfg中[pytest]节不推荐[tool:pytest]pytest.ini/tox.ini仍为[pytest]pytest.set_trace()已不需要import pdb; pdb.set_trace()通过Node访问Module/Function/Class/...自 3.9 起告警import pytest后从pytest模块访问字符串 skipif/xfail 条件仍完全支持布尔条件 reason按需迁移pytest.config全局对象5.0 移除request.config/pytestconfigfixture十、延伸阅读Marker 新 API 的完整实现src/_pytest/nodes.pyadd_marker/iter_markers/get_closest_markerMarker 数据结构Mark、MarkDecorator、ParameterSetsrc/_pytest/mark/structures.py缓存插件实现与选项注册src/_pytest/cacheprovider.py字符串条件求值逻辑src/_pytest/skipping.py现代 autouse fixture 与pytest.param的官方说明doc/en/explanation/fixtures.rst、doc/en/example/parametrize.rst缓存用法doc/en/how-to/cache.rst历史笔记的价值不在于过时而在于它精确记录了每个现代 API 之所以长成今天模样的原因。读懂这些变更你不仅能在遗留代码面前游刃有余也能更深刻地理解 pytest 的设计取舍。【免费下载链接】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),仅供参考