我们知道pytest的benchmark测试可以用一种参数化的方式来组合测试参数,简化代码。
这就是mark.parameterize 装饰器
例子1:
import pytest
def add(x, y):
return x + y
@pytest.mark.parametrize("a, b, c", [
(1, 2, 3),
(2, 2, 4),
(5.6, 7.1, 12.7),
(-1, 1, 0),
([1], [2], [1,2]), # 这里可以增加很多用例
])
def test_add(a, b, c):
assert add(a, b) == c
例子2 是带有固件的参数
@pytest.fixture(scope="function")
def address(request):
print(request.param)
cfg = request.param
print(f"process {cfg}")
@pytest.mark.parametrize("address", [
{
"addr": "shenzhen"
},
{
"addr": "shanghai"
}# 这里可以继续增加场景
], indirect=True)
def test_func(address):
print(address)
# TODO
indirect参数是让参数透传到固件的 request.param 中处理
这种用法常见于测试前前置不同的环境。比如连接不同的db
我们看看这个装饰器是如何起作用的?—— 这是为什么pytest的框架值得研究的地方,在mark装饰器这里,体现出很多工程技巧。
对于不熟悉装饰器的人而言,会惊叹——啊,装饰器竟然可以这么写。
在 pytest 的 structure.py模块里可以看到 mark生成器的细节。
首先我们看 pytest.mark.xxx 的索引,调到定义处,发现
mark是一个全局对象的别名
MARK_GEN = MarkGenerator(_ispytest=True)
在
pytest/init.py 我们清楚地可以找到这一行
from _pytest.mark import MARK_GEN as mark
mark即MARK_GEN的别名
MarkGenerator的实现概要
MarkDecorator 的对象的工厂类,每当调用 pytest.mark时,唤起 MARK_GEN这个单例。
class MarkGenerator:
skip: _SkipMarkDecorator
skipif: _SkipifMarkDecorator
xfail: _XfailMarkDecorator
parametrize: _ParametrizeMarkDecorator
usefixtures: _UsefixturesMarkDecorator
filterwarnings: _FilterwarningsMarkDecorator
def __init__(self, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
self._config: Optional[Config] = None
self._markers: Set[str] = set()
def __getattr__(self, name: str) -> MarkDecorator:
"""Generate a new :class:`MarkDecorator` with the given name."""
if name[0] == "_":
raise AttributeError("Marker name must NOT start with underscore")
if self._config is not None:
# We store a set of markers as a performance optimisation - if a mark
# name is in the set we definitely know it, but a mark may be known and
# not in the set. We therefore start by updating the set!
if name not in self._markers:
for line in self._config.getini("markers"):
# example lines: "skipif(condition): skip the given test if..."
# or "hypothesis: tests which use Hypothesis", so to get the
# marker name we split on both `:` and `(`.
marker = line.split(":")[0].split("(")[0].strip()
self._markers.add(marker)
# If the name is not in the set of known marks after updating,
# then it really is time to issue a warning or an error.
if name not in self._markers:
if self._config.option.strict_markers or self._config.option.strict:
fail(
f"{name!r} not found in `markers` configuration option",
pytrace=False,
)
# Raise a specific error for common misspellings of "parametrize".
if name in ["parameterize", "parametrise", "parameterise"]:
__tracebackhide__ = True
fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
warnings.warn(
"Unknown pytest.mark.%s - is this a typo? You can register "
"custom marks to avoid this warning - for details, see "
"https://docs.pytest.org/en/stable/how-to/mark.html" % name,
PytestUnknownMarkWarning,
2,
)
return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True)
这是一个marker生成器,最终会返回一个装饰器,但是分两种基本的情况。
一种是默认的六种装饰器 :
skip: _SkipMarkDecorator
skipif: _SkipifMarkDecorator
xfail: _XfailMarkDecorator
parametrize: _ParametrizeMarkDecorator
usefixtures: _UsefixturesMarkDecorator
filterwarnings: _FilterwarningsMarkDecorator
另外一种是从配置读出来的标记
这个配置就是 "pytest.ini"
MarkGenerator 的作用是生产MarkDecorator ,pytest提供了6中基本装饰器,然后从 pytest.ini中读进
样例:
[pytest]
markers: # 这个名字一定是markers 因为代码中硬编码(在__getattr__ 中)
smoke
下面要看下MarkDecorator的构造,它是如何修饰函数的。
源码中有一段文档注释是解释MarkDecorator如何作用的
摘录如下:
A decorator for applying a mark on test functions and classes.
``MarkDecorators`` are created with ``pytest.mark``:
mark1 = pytest.mark.NAME # Simple MarkDecorator
mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
and can then be applied as decorators to test functions::
@mark2
def test_function():
pass
When a ``MarkDecorator`` is called, it does the following:
1. If called with a single class as its only positional argument and no
additional keyword arguments, it attaches the mark to the class so it
gets applied automatically to all test cases found in that class.
2. If called with a single function as its only positional argument and
no additional keyword arguments, it attaches the mark to the function,
containing all the arguments already stored internally in the
``MarkDecorator``.
3. When called in any other case, it returns a new ``MarkDecorator``
instance with the original ``MarkDecorator``'s content updated with
the arguments passed to this call.
Note: The rules above prevent a ``MarkDecorator`` from storing only a
single function or class reference as its positional argument with no
additional keyword or positional arguments. You can work around this by
using `with_args()`.
当一个MarkDecorator被调用的时候,它做如下事情
- 如果使用单个类作为其唯一的位置参数来调用,并且没有附加的关键字参数,它将mark关联到类上,这时候会自动应用于该类中所有的测试用例。
- 如果使用单个函数作为其唯一的位置参数来调用,并且没有额外的关键字参数,它将标记附加到函数上,包含已内部存储的所有参数
“MarkDecorator”。
比如
@pytest.mark.xyz
def test_func():
...
@pytest.mark.xyz
class TestSomeSuit:
def test_case1():
...
def test_case2():
...
修饰类时,第一个位置参数是类对象,而且没有其它关键字参数,会修饰整个类所有的测试函数
- 其它情形,则会组装一个新的 MarkDecorator 返回
其它情形包括 带参数的装饰器
比如
@pytest.mark.skip(reason="wo buxiang ce")
def test_foo():
...
通过with_args(*args, **kwargs) 辅助函数重新生成一个新的MarkDecorator。
新的MarkDecorator会和原来的装饰器标记的mark——注意每个MarkDecorator内部持有一个mark对象——合成一个新的mark(使用combined_with函数)
这个新的MarkDecorator
class _SkipMarkDecorator(MarkDecorator):
@overload # type: ignore[override,misc,no-overload-impl]
def __call__(self, arg: Markable) -> Markable:
...
@overload
def __call__(self, reason: str = ...) -> "MarkDecorator":
...