英文:
pytest: manually add test to discovered tests
问题
@pytest.mark.mymark
def custom_assert():
assert True
如何强制 pytest 发现此测试?
通常情况下,如何将任何测试动态添加到 pytest 的已发现测试列表中,即使它们不符合命名约定?
英文:
# tests/test_assert.py
@pytest.mark.mymark
def custom_assert():
assert True
How do I force pytest to discover this test?
In general, how do I dynamically add any test to pytest's list of discovered tests, even if they don't fit in the naming convention?
答案1
得分: 3
pytest是相当可定制的,但你需要查看其广泛的API。幸运的是,代码基于静态类型,因此你可以轻松地从函数和类导航到其他函数和类。
首先,理解*pytest是如何发现测试的很有帮助。回想一下可配置的发现命名约定:
# pytest.ini的内容
# 示例1:使pytest查找"check"而不是"test"
[pytest]
python_files = check_*.py
python_classes = Check
python_functions = *_check
这意味着,例如,python_functions
的值在某处用于筛选出不被视为测试函数的函数。在pytest存储库上进行快速搜索,你可以看到这个:
class PyCollector(PyobjMixin, nodes.Collector):
def funcnamefilter(self, name: str) -> bool:
return self._matches_prefix_or_glob_option("python_functions", name)
PyCollector
是pytest Module
对象的基类,而module_: pytest.Module
具有obj
属性,该属性是types.ModuleType
对象本身。除了访问funcnamefilter::name
参数,你可以创建一个pytest.Module
、pytest.Package
和pytest.Class
的子类,来覆盖funcnamefilter
以接受使用你自定义的@pytest.mark.mymark
装饰器装饰的函数作为测试函数:
from __future__ import annotations
import types
import typing as t
import pytest
# 静态类型友好性
if t.TYPE_CHECKING:
from _pytest.python import PyCollector
class _MarkDecorated(t.Protocol):
pytestmark: list[pytest.Mark]
def __call__(self, *args: object, **kwargs: object) -> None:
"""测试函数回调方法"""
else:
PyCollector: t.TypeAlias = object
def _isPytestMarkDecorated(obj: object) -> t.TypeGuard[_MarkDecorated]:
"""
在函数上装饰`@pytest.mark.mymark`会得到这个结果:
>>> @pytest.mark.mymark
... def f() -> None:
... pass
...
>>> f.pytestmark
[Mark(name='mymark', args=(), kwargs={})]
这里的`Mark`是`pytest.Mark`。
该函数提供了一个用于静态类型目的的类型保护。
"""
if (
callable(obj)
and hasattr(obj, "pytestmark")
and isinstance(obj.pytestmark, list)
):
return True
return False
class _MyMarkMixin(PyCollector):
def funcnamefilter(self, name: str) -> bool:
underlying_py_obj: object = self.obj
assert isinstance(underlying_py_obj, (types.ModuleType, type))
func: object = getattr(underlying_py_obj, name)
if _isPytestMarkDecorated(func) and any(
mark.name == "mymark" for mark in func.pytestmark
):
return True
return super().funcnamefilter(name)
class MyMarkModule(_MyMarkMixin, pytest.Module):
pass
最后一件要做的事情是配置pytest,使其在收集测试模块时使用你的MyMarkModule
,而不是pytest.Module
。你可以使用每个目录的插件模块文件 conftest.py
来实现这一点,在这个文件中,你需要覆盖钩子pytest.pycollect.makemodule
(请参考pytest的实现来了解如何正确编写):
# conftest.py
import typing as t
from <...> import MyMarkModule
if t.TYPE_CHECKING:
import pathlib
import pytest
def pytest_pycollect_makemodule(
module_path: pathlib.Path, parent: object
) -> pytest.Module | None:
if module_path.name != "__init__.py":
return MyMarkModule.from_parent(parent, path=module_path) # type: ignore[no-any-return]
现在你可以运行pytest <your test file>
,你应该会看到所有带有@pytest.mark.mymark
装饰器的函数都作为测试函数运行,无论它们是否根据pytest_functions
配置设置命名。
这只是开始,关于如何使用pytest需要做和可以做的事情。如果你计划在其他地方使用@pytest.mark.mymark
,你还需要对pytest.Class
和pytest.Package
进行相同的操作。
英文:
pytest is fairly customisable, but you'll have to look at its extensive API. Luckily, the code base is statically typed, so you can navigate from functions and classes to other functions and classes fairly easily.
To start off, it pays to understand how pytest discovers tests. Recall the configurable discovery naming conventions:
# content of pytest.ini
# Example 1: have pytest look for "check" instead of "test"
[pytest]
python_files = check_*.py
python_classes = Check
python_functions = *_check
This implies that, for example, the value to python_functions
is used somewhere to filter out functions that are not considered as test functions. Do a quick search on the pytest repository to see this:
class PyCollector(PyobjMixin, nodes.Collector):
def funcnamefilter(self, name: str) -> bool:
return self._matches_prefix_or_glob_option("python_functions", name)
PyCollector
is a base class for pytest Module
objects, and module_: pytest.Module
has an obj
property which is the types.ModuleType
object itself. Along with access to the funcnamefilter::name
parameter, you can make a subclass of pytest.Module
, pytest.Package
, and pytest.Class
to override funcnamefilter
to accept functions decorated your custom @pytest.mark.mymark
decorator as test functions:
from __future__ import annotations
import types
import typing as t
import pytest
# Static-type-friendliness
if t.TYPE_CHECKING:
from _pytest.python import PyCollector
class _MarkDecorated(t.Protocol):
pytestmark: list[pytest.Mark]
def __call__(self, *args: object, **kwargs: object) -> None:
"""Test function callback method"""
else:
PyCollector: t.TypeAlias = object
def _isPytestMarkDecorated(obj: object) -> t.TypeGuard[_MarkDecorated]:
"""
Decorating `@pytest.mark.mymark` over a function results in this:
>>> @pytest.mark.mymark
... def f() -> None:
... pass
...
>>> f.pytestmark
[Mark(name='mymark', args=(), kwargs={})]
where `Mark` is `pytest.Mark`.
This function provides a type guard for static typing purposes.
"""
if (
callable(obj)
and hasattr(obj, "pytestmark")
and isinstance(obj.pytestmark, list)
):
return True
return False
class _MyMarkMixin(PyCollector):
def funcnamefilter(self, name: str) -> bool:
underlying_py_obj: object = self.obj
assert isinstance(underlying_py_obj, (types.ModuleType, type))
func: object = getattr(underlying_py_obj, name)
if _isPytestMarkDecorated(func) and any(
mark.name == "mymark" for mark in func.pytestmark
):
return True
return super().funcnamefilter(name)
class MyMarkModule(_MyMarkMixin, pytest.Module):
pass
The last thing to do is to configure pytest to use your MyMarkModule
rather than pytest.Module
when collecting test modules. You can do this with the per-directory plugin module file conftest.py
, where you would override the hook pytest.pycollect.makemodule
(please see pytest's implementation on how to write this properly):
# conftest.py
import typing as t
from <...> import MyMarkModule
if t.TYPE_CHECKING:
import pathlib
import pytest
def pytest_pycollect_makemodule(
module_path: pathlib.Path, parent: object
) -> pytest.Module | None:
if module_path.name != "__init__.py":
return MyMarkModule.from_parent(parent, path=module_path) # type: ignore[no-any-return]
Now you can run pytest <your test file>
and you should see all @pytest.mark.mymark
functions run as test functions, regardless of whether they're named according to the pytest_functions
configuration setting.
This is a start on what you need to do, and can do with pytest. You'll have to do this with pytest.Class
and pytest.Package
as well, if you're planning on using @pytest.mark.mymark
elsewhere.
答案2
得分: 1
更改命名规范
> 您可以通过在配置文件中设置 python_files、python_classes 和 python_functions 来配置不同的命名约定。以下是一个示例:
> ini > # pytest.ini 文件的内容 > # 示例 1:让 pytest 查找“check”而不是“test” > [pytest] > python_files = check_*.py > python_classes = Check > python_functions = *_check >
> 这将使 pytest 在匹配 check_*.py 理模式的文件中查找测试,以及在类中匹配 Check 前缀以及匹配 *_check 的函数和方法。
> 您可以通过在模式之间添加空格来检查多个 glob 模式。
所以,在您的情况下,您需要在 pytest.ini
中使用以下配置:
# pytest.ini 文件的内容
[pytest]
python_functions = *_assert
英文:
Change naming convention
Pytest > Changing naming conventions
> You can configure different naming conventions by setting the python_files, python_classes and python_functions in your configuration file. Here is an example:
> ini
> # content of pytest.ini
> # Example 1: have pytest look for "check" instead of "test"
> [pytest]
> python_files = check_*.py
> python_classes = Check
> python_functions = *_check
>
> This would make pytest look for tests in files that match the check_* .py glob-pattern, Check prefixes in classes, and functions and methods that match *_check.
> You can check for multiple glob patterns by adding a space between the patterns.
So, in your case, you need this configuration in pytest.ini
:
# content of pytest.ini
[pytest]
python_functions = *_assert
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论