编写插件¶
为自己的项目实现本地 conftest 插件或可通过 pip 安装的插件非常容易,这些插件可以在许多项目中(包括第三方项目)使用。如果您只想使用插件而不是编写插件,请参阅如何安装和使用插件。
一个插件包含一个或多个 hook 函数。编写 hook 解释了如何自己编写 hook 函数的基础知识和细节。pytest
通过调用以下插件的良好指定的 hook 来实现配置、收集、运行和报告的所有方面
内置插件:从 pytest 的内部
_pytest
目录加载。conftest.py 插件:在测试目录中自动发现的模块
原则上,每个 hook 调用都是一个 1:N
Python 函数调用,其中 N
是给定规范的已注册实现函数的数量。所有规范和实现都遵循 pytest_
前缀命名约定,使其易于区分和查找。
工具启动时的插件发现顺序¶
pytest
在工具启动时按以下方式加载插件模块
通过扫描命令行中的
-p no:name
选项并阻止加载该插件(即使是内置插件也可以通过这种方式阻止)。这发生在正常的命令行解析之前。通过加载所有内置插件。
通过扫描命令行中的
-p name
选项并加载指定的插件。这发生在正常的命令行解析之前。通过加载通过已安装第三方软件包入口点注册的所有插件,除非设置了
PYTEST_DISABLE_PLUGIN_AUTOLOAD
环境变量。通过加载通过
PYTEST_PLUGINS
环境变量指定的所有插件。通过加载所有“初始”
conftest.py
文件确定测试路径:在命令行上指定,否则如果在根目录运行,则在
testpaths
中定义,否则为当前目录对于每个测试路径,如果存在,则加载相对于测试路径目录部分的
conftest.py
和test*/conftest.py
。在加载conftest.py
文件之前,加载其所有父目录中的conftest.py
文件。加载conftest.py
文件后,递归加载其pytest_plugins
变量(如果存在)中指定的所有插件。
conftest.py:本地按目录插件¶
本地 conftest.py
插件包含特定于目录的 hook 实现。Hook 会话和测试运行活动将调用在文件系统根目录附近的 conftest.py
文件中定义的所有 hook。以下是实现 pytest_runtest_setup
hook 的示例,以便为 a
子目录中的测试调用该 hook,但不为其他目录调用
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"
注意
如果您有 conftest.py
文件,它们不驻留在 python 包目录中(即包含 __init__.py
的目录),那么“import conftest”可能会有歧义,因为您的 PYTHONPATH
或 sys.path
上也可能存在其他 conftest.py
文件。因此,对于项目而言,最佳实践是将 conftest.py
放在包作用域下,或者永远不要从 conftest.py
文件导入任何内容。
注意
由于 pytest 在启动期间发现插件的方式,某些 hook 无法在非初始的 conftest.py 文件中实现。有关详细信息,请参阅每个 hook 的文档。
编写您自己的插件¶
如果您想编写插件,可以从许多实际示例中复制
自定义收集示例插件:用于在 Yaml 文件中指定测试的基本示例
提供 pytest 自身功能的内置插件
许多外部插件提供附加功能
所有这些插件都实现了hook和/或fixture来扩展和添加功能。
注意
请务必查看优秀的 cookiecutter-pytest-plugin 项目,这是一个用于创作插件的 cookiecutter 模板。
该模板提供了一个出色的起点,其中包含一个可工作的插件、使用 tox 运行的测试、全面的 README 文件以及预配置的入口点。
一旦您的插件拥有除您自己之外的一些快乐用户,也请考虑将您的插件贡献给 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
作为插件,该插件可以定义 hook。使用 pytest --trace-config
确认注册
注意
确保在您的 PyPI 分类器列表中包含 Framework :: Pytest
,以便用户可以轻松找到您的插件。
断言重写¶
pytest
的主要功能之一是使用简单的断言语句以及在断言失败时对表达式进行详细的内省。这是通过“断言重写”提供的,它在解析的 AST 被编译为字节码之前对其进行修改。这是通过 PEP 302 导入 hook 完成的,该 hook 在 pytest
启动时很早就安装,并在导入模块时执行此重写。但是,由于我们不想测试与您将在生产环境中运行的字节码不同的字节码,因此此 hook 仅重写测试模块本身(由 python_files
配置选项定义),以及属于插件的任何模块。任何其他导入的模块都不会被重写,并且会发生正常的断言行为。
如果您在其他模块中有断言助手,您需要在其中启用断言重写,则需要明确要求 pytest
在导入此模块之前重写它。
- register_assert_rewrite(*names)[源代码]
注册一个或多个要在导入时重写的模块名称。
此函数将确保此模块或包内的所有模块的断言语句都将被重写。因此,您应该确保在实际导入模块之前调用此函数,如果您是使用包的插件,则通常在您的 __init__.py 中调用。
- 参数:
names (str) – 要注册的模块名称。
当您编写使用包创建的 pytest 插件时,这一点尤其重要。导入 hook 仅将 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
将被重写。如果 helper 模块还包含需要重写的断言语句,则需要在导入之前将其标记为重写。最简单的方法是在 __init__.py
模块中标记它以进行重写,当导入包内的模块时,该模块将始终首先被导入。这样,plugin.py
仍然可以正常导入 helper.py
。pytest_foo/__init__.py
的内容然后需要看起来像这样
import pytest
pytest.register_assert_rewrite("pytest_foo.helper")
在测试模块或 conftest 文件中要求/加载插件¶
您可以使用 pytest_plugins
在测试模块或 conftest.py
文件中要求插件
pytest_plugins = ["name1", "name2"]
当加载测试模块或 conftest 插件时,指定的插件也将被加载。任何模块都可以被指定为插件,包括内部应用程序模块
pytest_plugins = "myapp.testsupport.myplugin"
pytest_plugins
是递归处理的,因此请注意,在上面的示例中,如果 myapp.testsupport.myplugin
也声明了 pytest_plugins
,则变量的内容也将作为插件加载,依此类推。
注意
不推荐在非根 conftest.py
文件中使用 pytest_plugins
变量来要求插件。
这很重要,因为 conftest.py
文件实现了按目录的 hook 实现,但是一旦导入插件,它将影响整个目录树。为了避免混淆,不推荐在任何不位于测试根目录中的 conftest.py
文件中定义 pytest_plugins
,并且会引发警告。
这种机制使得在应用程序甚至外部应用程序中共享 fixture 变得容易,而无需使用 入口点打包元数据 技术创建外部插件。
通过 pytest_plugins
导入的插件也将自动标记为断言重写(请参阅 pytest.register_assert_rewrite()
)。但是,为了使这产生任何影响,模块必须尚未导入;如果在处理 pytest_plugins
语句时模块已经导入,则会产生警告,并且插件内部的断言将不会被重写。要解决此问题,您可以在导入模块之前自己调用 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"]
或者,您可以使用 -p pytester
命令行选项调用 pytest。
这将允许您使用 pytester
fixture 来测试您的插件代码。
让我们通过一个示例演示您可以使用该插件做什么。假设我们开发了一个插件,该插件提供了一个 fixture hello
,它产生一个函数,我们可以使用一个可选参数调用此函数。如果我们不提供值,它将返回字符串值 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.py
文件尤其有用。
请注意,为了使 pytester.copy_example
工作,我们需要在 pytest.ini
中设置 pytester_example_dir
,以告知 pytest 在哪里查找示例文件。
# content of pytest.ini
[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-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
configfile: pytest.ini
collected 2 items
test_example.py .. [100%]
============================ 2 passed in 0.12s =============================
有关 runpytest()
返回的结果对象以及它提供的方法的更多信息,请查看 RunResult
文档。