编写 pytest 插件
为自己的项目实现本地 conftest 插件并不困难;也可以编写通过 pip 安装的插件,供多个项目,包括第三方项目使用。如果只是想使用插件,请参阅如何安装和使用插件。
一个插件包含一个或多个钩子函数。编写钩子介绍了自行编写钩子函数的基础与细节。pytest 通过调用以下插件中明确定义的钩子,实现配置、收集、运行和报告的各个环节:
- 内置插件:从 pytest 内部的
_pytest目录载入。 - 外部插件:已安装的第三方模块,通过打包元数据中的入口点发现。
- conftest.py 插件:在测试目录中自动发现的模块。
原则上,每次钩子调用都是一次 1:N 的 Python 函数调用,N 表示针对某个规范注册的实现函数数量。所有规范和实现都遵循 pytest_ 前缀命名约定,便于识别与查找。
启动时的插件发现顺序
pytest 启动时按以下顺序载入插件模块:
- 扫描命令行中的
-p no:name选项,禁止对应插件载入。内置插件也可以这样禁用。这发生在常规命令行解析之前。 - 载入所有内置插件。
- 扫描命令行中的
-p name选项,载入指定插件。这同样发生在常规命令行解析之前。 - 载入通过已安装第三方包的入口点注册的所有插件,除非设置了
PYTEST_DISABLE_PLUGIN_AUTOLOAD环境变量。 - 载入通过
PYTEST_PLUGINS环境变量指定的插件。 - 载入所有“初始”
conftest.py文件。先确定测试路径:优先采用命令行指定路径;否则,如果在 rootdir 运行且定义了testpaths,则使用它;再否则使用当前目录。对于每条测试路径,载入相对于其目录部分的conftest.py和test*/conftest.py(如果存在)。载入一个conftest.py前,先载入其所有父目录中的同名文件;载入后,递归载入其pytest_plugins变量中指定的插件(如果有)。
conftest.py:按目录生效的本地插件
本地 conftest.py 插件包含特定目录的钩子实现。钩子会话与测试运行活动会调用更靠近文件系统根目录的 conftest.py 文件中定义的所有钩子。例如,下面的 pytest_runtest_setup 实现只针对 a 子目录的测试调用,不作用于其他目录:
a/conftest.py:
def pytest_runtest_setup(item):
# called for running each test in 'a' directory
print("setting up", item)
a/test_sub.py:
def test_sub():
pass
test_flat.py:
def test_flat():
pass
可以这样运行:
pytest test_flat.py --capture=no # will not show "setting up"
pytest a/test_sub.py --capture=no # will show "setting up"
编写自己的插件
可以参考许多实际插件:
- 自定义收集插件示例:用 YAML 文件指定测试的基本示例。
- 提供 pytest 自身功能的内置插件。
- 提供额外功能的外部插件。
当插件拥有你之外的满意用户后,也可以考虑将插件贡献给 pytest-dev。
让其他人可以安装插件
如果希望对外提供插件,可以为发行包定义入口点,让 pytest 找到插件模块。入口点由打包工具提供。
pytest 查找 pytest11 入口点来发现插件,因此可以在 pyproject.toml 中定义:
# sample ./pyproject.toml file
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "myproject"
classifiers = [
"Framework :: Pytest",
]
[project.entry-points.pytest11]
myproject = "myproject.pluginmodule"
按这种方式安装包后,pytest 会将 myproject.pluginmodule 作为插件载入,插件中可以定义钩子。使用 pytest --trace-config 确认注册。
断言重写
pytest 的主要特性之一,是使用普通 assert 语句,并在断言失败时详细解析表达式。这由断言重写实现:在解析得到的 AST 编译成字节码之前修改它。pytest 启动早期会安装一个 PEP 302 导入钩子,在模块导入时执行重写。
为了避免测试的字节码与生产环境实际运行的字节码不同,该钩子只重写测试模块自身(由 python_files 配置决定)和属于插件的模块。其他导入模块不会被重写,仍使用普通断言行为。
如果其他模块中的断言辅助函数也需要重写,必须在模块导入前明确请求 pytest 重写它。
register_assert_rewrite(*names)
注册一个或多个需要在导入时重写的模块名称。它确保该模块或包内所有模块的 assert 语句被重写,因此应在实际导入之前调用。对于以包形式组织的插件,通常放在 __init__.py 中。参数 names 是待注册模块的名称。
这对采用包结构的 pytest 插件尤其重要。导入钩子仅将 conftest.py 和 pytest11 入口点列出的模块视作插件。考虑下面的包:
pytest_foo/__init__.py
pytest_foo/plugin.py
pytest_foo/helper.py
以及典型的 setup.py 片段:
setup(..., entry_points={"pytest11": ["foo = pytest_foo.plugin"]}, ...)
这种情况下,仅 pytest_foo/plugin.py 会被重写。如果辅助模块也包含需要重写的 assert 语句,就必须在导入前标记。最简单的方法是在 __init__.py 中注册,因为导入包内模块时,它总会先被导入。这样 plugin.py 仍可正常导入 helper.py。相应的 pytest_foo/__init__.py 内容应为:
import pytest
pytest.register_assert_rewrite("pytest_foo.helper")
在测试模块或 conftest 文件中请求、载入插件
可以在测试模块或 conftest.py 中用 pytest_plugins 请求插件:
pytest_plugins = ["name1", "name2"]
测试模块或 conftest 插件载入时,指定插件也会载入。任何模块,包括应用内部模块,都可以被指定为插件:
pytest_plugins = "myapp.testsupport.myplugin"
pytest_plugins 会递归处理。如果示例中的 myapp.testsupport.myplugin 也声明了此变量,其内容也会作为插件载入,依此类推。
这一机制使应用内部甚至不同应用之间共享 fixture 更方便,无需通过入口点打包元数据建立外部插件。
通过 pytest_plugins 导入的插件还会自动标记为需要断言重写,参见 pytest.register_assert_rewrite()。但是,模块必须尚未被导入。如果处理变量时模块已经导入,会产生警告,插件中的断言也不会重写。
可以在导入模块前自行调用 pytest.register_assert_rewrite,也可以调整代码,将导入延后到插件注册之后。
按名称访问其他插件
插件若需要与另一个插件的代码协作,可以通过插件管理器获得其引用:
plugin = config.pluginmanager.get_plugin("name_of_plugin")
使用 --trace-config 选项可以查看已有插件名称。
注册自定义标记
如果插件使用任何标记,就应注册它们,使其出现在 pytest 帮助文本中,并避免无谓的警告。下面的插件为所有用户注册 cool_marker 与 mark_with:
def pytest_configure(config):
config.addinivalue_line("markers", "cool_marker: this one is for cool tests.")
config.addinivalue_line(
"markers", "mark_with(arg, arg2): this marker takes arguments."
)
测试插件
pytest 自带 pytester 插件,用来为插件代码编写测试。默认情况下它被禁用,需要先启用。
在测试目录的 conftest.py 中加入:
# content of conftest.py
pytest_plugins = ["pytester"]
也可以在调用 pytest 时使用 -p pytester。这样就可以用 pytester fixture 测试插件。
假设编写了一个插件,它提供的 hello fixture 返回一个函数。调用该函数时可以传入一个可选参数。不传值时返回 Hello World!;传入字符串时返回 Hello {value}!:
import pytest
def pytest_addoption(parser):
group = parser.getgroup("helloworld")
group.addoption(
"--name",
action="store",
dest="name",
default="World",
help='Default "name" for hello().',
)
@pytest.fixture
def hello(request):
name = request.config.getoption("name")
def _hello(name=None):
if not name:
name = request.config.getoption("name")
return f"Hello {name}!"
return _hello
pytester fixture 提供方便的 API,用于创建临时 conftest.py 和测试文件,还可以运行测试并返回结果对象,以便断言测试结果:
def test_hello(pytester):
"""Make sure that our plugin works."""
# create a temporary conftest.py file
pytester.makeconftest(
"""
import pytest
@pytest.fixture(params=[
"Brianna",
"Andreas",
"Floris",
])
def name(request):
return request.param
"""
)
# create a temporary pytest test file
pytester.makepyfile(
"""
def test_hello_default(hello):
assert hello() == "Hello World!"
def test_hello_name(hello, name):
assert hello(name) == "Hello {0}!".format(name)
"""
)
# run all tests with pytest
result = pytester.runpytest()
# check that all 4 tests passed
result.assert_outcomes(passed=4)
也可以先把示例复制到 pytester 的隔离环境,再在其中运行 pytest。这样能将被测试逻辑分离到独立文件中,对较长的测试或 conftest 文件尤其有用。
要让 pytester.copy_example 工作,需要在配置文件中设置 pytester_example_dir,告诉 pytest 到哪里查找示例文件:
# content of pytest.toml
[pytest]
pytester_example_dir = "."
# content of test_example.py
def test_plugin(pytester):
pytester.copy_example("test_example.py")
pytester.runpytest("-k", "test_example")
def test_example():
pass
原文给出的运行输出如下:
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
configfile: pytest.toml
collected 2 items
test_example.py .. [100%]
============================ 2 passed in 0.12s =============================
关于 runpytest() 返回的结果对象及其方法,更多信息请查看 RunResult 文档。
来源与许可
来源:pytest stable 文档:Writing plugins。文档版权 © 2015 holger krekel 和 pytest-dev 团队。本文翻译正文并调整排版,保留 stable 页面示例;示例输出来自原文,不是本地实跑。MIT 许可完整保留如下:
The MIT License (MIT)
Copyright (c) 2004 Holger Krekel and others
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.











暂无评论内容