如何从PyTest插件动态创建Fixture并注入测试用例?
需要开发PyTest插件,实现动态创建Fixture并注入现有测试用例,用于兼容遗留测试的setup/teardown钩子;要求支持module作用域的Fixture。尝试用pytest_collection_modifyitems钩子实现时,代码在模块内运行正常,但作为插件运行时,测试无法识别注入的Fixture。
最小复现代码
plugin.py
from typing import List, ModuleType import pytest from _pytest.python import Function from _pytest.nodes import Item def pytest_collection_modifyitems(items: List[Item]): for item in items: if not isinstance(item, Function): continue pytest_function: Function = item.getparent(Function) def _generate_fixture(name: str): @pytest.fixture(name=name) def _fixture(): print("Setup...") yield print("Teardown...") return _fixture def inject_fixture(name: str, module: ModuleType): setattr(module, name, _generate_fixture(name)) inject_fixture("example_fixture", pytest_function.module) pytest_function: Function = item.getparent(Function) pytest_function.fixturenames.append("example_fixture")
test_dummy.py
def test_dummy_func(): print(globals()) pass
预期与实际行为
- 预期:执行
pytest test_dummy.py -r P时,控制台输出Setup...和Teardown... - 实际:测试报错找不到Fixture,错误信息:
============================ ERRORS =============================
_______________ ERROR at setup of test_dummy_func _______________
file /local/home/harleyip/workspaces/LambdaWorkerPythonTestDriver/src/LambdaWorkerPythonTestDriver/test_dummy.py, line 3
def test_dummy_func():
E fixture 'example_fixture' not foundavailable fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory use 'pytest --fixtures [testpath]' for help on them.
- 禁用Fixture依赖后,测试打印
globals()显示example_fixture已存在于模块全局变量中:
----------Captured stdout call--------------
...
'example_fixture': <function pytest_collection_modifyitems..generate_fixture. ._fixture at 0x7fc6336daf70>}
...
解决方法
问题根源在于pytest_collection_modifyitems钩子执行时,pytest已完成测试模块的Fixture扫描工作,此时往模块添加Fixture函数不会被识别。需在Fixture扫描阶段之前注入,或直接通过pytest API注册Fixture。
方案1:提前注入Fixture到模块
使用pytest_pycollect_makeitem钩子(测试项创建时触发,早于Fixture扫描)注入Fixture:
from typing import Optional import pytest from _pytest.python import Function, PyCollector from _pytest.nodes import Item def pytest_pycollect_makeitem(collector: PyCollector, name: str, obj): # 收集到测试函数时,先往模块注入Fixture if isinstance(obj, type(lambda: None)) and name.startswith("test_"): def _generate_fixture(fixture_name: str): @pytest.fixture(name=fixture_name, scope="module") # 可指定作用域 def _fixture(): print("Setup...") yield print("Teardown...") return _fixture fixture_name = "example_fixture" setattr(collector.module, fixture_name, _generate_fixture(fixture_name)) # 执行默认收集逻辑 return None def pytest_collection_modifyitems(items: list[Item]): for item in items: if isinstance(item, Function) and "example_fixture" not in item.fixturenames: item.fixturenames.append("example_fixture")
方案2:全局注册Fixture
通过pytest_configure钩子直接将Fixture注册到pytest的插件管理器,无需注入模块:
import pytest from typing import List from _pytest.nodes import Item from _pytest.python import Function def pytest_configure(config): def _example_fixture(): print("Setup...") yield print("Teardown...") # 注册module作用域的Fixture config.pluginmanager.register( pytest.fixture(name="example_fixture", scope="module")(_example_fixture) ) def pytest_collection_modifyitems(items: List[Item]): for item in items: if isinstance(item, Function) and "example_fixture" not in item.fixturenames: item.fixturenames.append("example_fixture")
方案说明
- 方案1保留模块级Fixture的独立性,适合需要为不同模块定制Fixture的场景
- 方案2实现更简洁,全局注册Fixture,适合无需模块定制的通用场景
- 两种方案均支持指定
scope参数(如module、function等)
内容的提问来源于stack exchange,提问作者patha454

