文章目录
问题背景
在使用 pytest 进行测试时,我遇到了这样的错误:
Defining 'pytest_plugins' in a non-top-level conftest is no longer supported: It affects the entire test suite instead of just below the conftest as expected.
这个错误通常出现在测试工程的结构中有多层 conftest.py 文件,并且在非顶层的 conftest 中定义了 pytest_plugins
。从 pytest 7.0.0 版本开始,这种用法被废弃,因为它会影响整个测试套件而不仅仅是该 conftest.py 以下的测试。
案例中,测试工程根目录下有一个 conftest.py,其中包含:
python
pytest_plugins = ["my_python_lib.base.testbase.conftest"]
这里 my_python_lib
是一个自定义的 Python 第三方库,测试工程中的用例需要调用 my_python_lib.base.testbase.conftest
中的 fixture。
最佳解决方案:将库转换为真正的 pytest 插件
将我们的库转换为一个真正的 pytest 插件是最优雅和最可维护的解决方案。这样不仅解决了当前问题,还提高了代码的可复用性和可扩展性。
步骤 1:重构库结构
首先,调整库结构,确保 fixture 代码位于合适的模块中:
my_python_lib/
├── __init__.py
├── base/
│ ├── __init__.py
│ ├── testbase/
│ │ ├── __init__.py
│ │ ├── fixture.py # 将 conftest.py 中的 fixture 移到这里
│ │ └── plugin.py # 新建的插件入口点文件
步骤 2:创建插件入口点文件
创建 plugin.py
文件,导入所有 fixture 并定义任何需要的 pytest 钩子:
python
# my_python_lib/base/testbase/plugin.py
from .fixture import * # 导入所有的 fixture
# 在这里可以定义 pytest 钩子函数
def pytest_configure(config):
"""
pytest 配置阶段被调用的钩子
可以在这里进行全局配置
"""
pass
步骤 3:修改库的 setup.py
在库的 setup.py 中添加 pytest 插件的入口点:
python
from setuptools import setup, find_packages
setup(
name="my_python_lib",
version="1.0.0",
packages=find_packages(),
description="我的测试工具库",
author="Your Name",
author_email="[email protected]",
# 添加 pytest 插件入口点,这里的 pytest11 是一个固定写法,了解到这个情况的我,感觉这简直"逆天"
entry_points={
'pytest11': [
'my_lib = my_python_lib.base.testbase.plugin',
],
},
# 添加依赖
install_requires=[
'pytest>=6.0.0',
# 其他依赖...
],
)
步骤 4:重新安装库
bash
pip uninstall -y my_python_lib # 先卸载当前版本
cd /path/to/my_python_lib
pip install -e . # 以开发模式安装
步骤 5:修改测试项目
删除测试项目中 conftest.py 中的 pytest_plugins
定义,因为现在插件会自动加载:
python
# 测试项目的 conftest.py
# 删除这一行:
# pytest_plugins = ["my_python_lib.base.testbase.conftest"]
# 可以添加其他测试项目特定的 fixture
def pytest_configure(config):
# 测试项目特定的配置
pass
步骤 6:验证插件是否正确安装
运行以下命令验证插件是否被正确识别:
bash
python -m pytest --trace-config
应该能看到类似这样的输出:
bash
pytest11 plugin registration SETUP: my_python_lib.base.testbase.plugin
代码示例
fixture.py
示例
python
# my_python_lib/base/testbase/fixture.py
import pytest
import os
import tempfile
@pytest.fixture
def temp_dir():
"""提供一个临时目录"""
with tempfile.TemporaryDirectory() as temp_dir:
yield temp_dir
@pytest.fixture
def temp_file():
"""提供一个临时文件"""
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
file_path = temp_file.name
yield file_path
# 测试后清理
if os.path.exists(file_path):
os.remove(file_path)
@pytest.fixture
def sample_data():
"""提供示例数据"""
return {
"name": "test",
"values": [1, 2, 3, 4, 5],
"metadata": {
"version": "1.0",
"type": "test-data"
}
}
plugin.py
完整示例
python
# my_python_lib/base/testbase/plugin.py
from .fixture import *
def pytest_configure(config):
"""配置 pytest 环境"""
config.addinivalue_line(
"markers", "slow: 标记执行较慢的测试"
)
def pytest_addoption(parser):
"""添加命令行选项"""
parser.addoption(
"--skip-slow",
action="store_true",
default=False,
help="跳过标记为 slow 的测试"
)
def pytest_collection_modifyitems(config, items):
"""修改收集的测试项"""
if config.getoption("--skip-slow"):
skip_slow = pytest.mark.skip(reason="跳过慢测试 (--skip-slow 选项)")
for item in items:
if "slow" in item.keywords:
item.add_marker(skip_slow)
测试示例
python
# 测试文件示例 test_utils.py
import pytest
import os
import json
def test_temp_dir_fixture(temp_dir):
"""测试临时目录 fixture"""
# 在临时目录创建文件
file_path = os.path.join(temp_dir, "test.txt")
with open(file_path, "w") as f:
f.write("Hello, World!")
# 验证文件创建成功
assert os.path.exists(file_path)
with open(file_path, "r") as f:
content = f.read()
assert content == "Hello, World!"
@pytest.mark.slow
def test_sample_data_manipulation(sample_data, temp_file):
"""测试数据操作(标记为慢测试)"""
# 将示例数据写入临时文件
with open(temp_file, "w") as f:
json.dump(sample_data, f)
# 读取并验证数据
with open(temp_file, "r") as f:
loaded_data = json.load(f)
assert loaded_data == sample_data
assert loaded_data["metadata"]["version"] == "1.0"
assert sum(loaded_data["values"]) == 15
使用方法
安装了这个 pytest 插件后,你可以在任何测试项目中直接使用这些 fixture,无需额外导入或配置:
-
安装你的库:
bashpip install my_python_lib
-
在测试文件中直接使用 fixture:
pythondef test_file_operations(temp_dir, temp_file): # 自动获取临时目录和临时文件 with open(temp_file, 'w') as f: f.write('测试内容') assert os.path.exists(temp_file)
-
使用示例数据 fixture:
pythondef test_data_processing(sample_data): # sample_data 自动可用 assert sample_data["name"] == "test" assert len(sample_data["values"]) == 5
-
跳过慢测试:
bashpython -m pytest --skip-slow
-
运行测试并查看所有可用的标记:
bashpython -m pytest --markers
这些 fixture 可以组合使用,也可以在自己的 conftest.py 中扩展它们,为它们提供自定义行为。
优势
- 符合 pytest 最佳实践 - 使用官方推荐的插件机制
- 避免警告和错误 - 不再使用不推荐的
pytest_plugins
方式 - 更好的可发现性 - 自动注册 fixture,无需显式导入
- 可配置性 - 可以添加命令行选项和配置项
- 模块化 - 更容易维护和扩展
- 可重用性 - 可以在多个项目中使用同一套测试工具
总结
通过将测试工具库转换为真正的 pytest 插件,我们不仅解决了特定的错误问题,还提高了代码质量和可维护性。这种方法虽然前期工作量稍大,但从长远来看更加健壮,尤其是当测试库需要在多个项目中使用时。