我做了一个有趣的装饰器,可以像这样编写fixture:
@fixture_taking_arguments
def dog(request, /, name, age=69):
return f"{name} the dog aged {age}"
这里,在 /
左侧是其他的固定内容,右侧则是使用以下方式提供的参数:
@dog.arguments("Buddy", age=7)
def test_with_dog(dog):
assert dog == "Buddy the dog aged 7"
这与函数参数的工作方式相同。如果你没有提供
age
参数,则会使用默认值
69
。如果你没有提供
name
,或省略了
dog.arguments
装饰器,就会得到常规的
TypeError: dog() missing 1 required positional argument: 'name'
。如果你有另一个需要参数
name
的fixture,它不会与此fixture冲突。
还支持异步fixture。
此外,这也为你提供了一个很好的设置计划:
$ pytest test_dogs_and_owners.py --setup-plan
SETUP F dog['Buddy', age=7]
...
SETUP F dog['Champion']
SETUP F owner (fixtures used: dog)['John Travolta']
一个完整的示例:
from plugin import fixture_taking_arguments
@fixture_taking_arguments
def dog(request, /, name, age=69):
return f"{name} the dog aged {age}"
@fixture_taking_arguments
def owner(request, dog, /, name="John Doe"):
yield f"{name}, owner of {dog}"
@dog.arguments("Buddy", age=7)
def test_with_dog(dog):
assert dog == "Buddy the dog aged 7"
@dog.arguments("Champion")
class TestChampion:
def test_with_dog(self, dog):
assert dog == "Champion the dog aged 69"
def test_with_default_owner(self, owner, dog):
assert owner == "John Doe, owner of Champion the dog aged 69"
assert dog == "Champion the dog aged 69"
@owner.arguments("John Travolta")
def test_with_named_owner(self, owner):
assert owner == "John Travolta, owner of Champion the dog aged 69"
装饰器的代码:
import pytest
from dataclasses import dataclass
from functools import wraps
from inspect import signature, Parameter, isgeneratorfunction, iscoroutinefunction, isasyncgenfunction
from itertools import zip_longest, chain
_NOTHING = object()
def _omittable_parentheses_decorator(decorator):
@wraps(decorator)
def wrapper(*args, **kwargs):
if not kwargs and len(args) == 1 and callable(args[0]):
return decorator()(args[0])
else:
return decorator(*args, **kwargs)
return wrapper
@dataclass
class _ArgsKwargs:
args: ...
kwargs: ...
def __repr__(self):
return ", ".join(chain(
(repr(v) for v in self.args),
(f"{k}={v!r}" for k, v in self.kwargs.items())))
def _flatten_arguments(sig, args, kwargs):
assert len(sig.parameters) == len(args) + len(kwargs)
for name, arg in zip_longest(sig.parameters, args, fillvalue=_NOTHING):
yield arg if arg is not _NOTHING else kwargs[name]
def _get_actual_args_kwargs(sig, args, kwargs):
request = kwargs["request"]
try:
request_args, request_kwargs = request.param.args, request.param.kwargs
except AttributeError:
request_args, request_kwargs = (), {}
return tuple(_flatten_arguments(sig, args, kwargs)) + request_args, request_kwargs
@_omittable_parentheses_decorator
def fixture_taking_arguments(*pytest_fixture_args, **pytest_fixture_kwargs):
def decorator(func):
original_signature = signature(func)
def new_parameters():
for param in original_signature.parameters.values():
if param.kind == Parameter.POSITIONAL_ONLY:
yield param.replace(kind=Parameter.POSITIONAL_OR_KEYWORD)
new_signature = original_signature.replace(parameters=list(new_parameters()))
if "request" not in new_signature.parameters:
raise AttributeError("Target function must have positional-only argument `request`")
is_async_generator = isasyncgenfunction(func)
is_async = is_async_generator or iscoroutinefunction(func)
is_generator = isgeneratorfunction(func)
if is_async:
@wraps(func)
async def wrapper(*args, **kwargs):
args, kwargs = _get_actual_args_kwargs(new_signature, args, kwargs)
if is_async_generator:
async for result in func(*args, **kwargs):
yield result
else:
yield await func(*args, **kwargs)
else:
@wraps(func)
def wrapper(*args, **kwargs):
args, kwargs = _get_actual_args_kwargs(new_signature, args, kwargs)
if is_generator:
yield from func(*args, **kwargs)
else:
yield func(*args, **kwargs)
wrapper.__signature__ = new_signature
fixture = pytest.fixture(*pytest_fixture_args, **pytest_fixture_kwargs)(wrapper)
fixture_name = pytest_fixture_kwargs.get("name", fixture.__name__)
def parametrizer(*args, **kwargs):
return pytest.mark.parametrize(fixture_name, [_ArgsKwargs(args, kwargs)], indirect=True)
fixture.arguments = parametrizer
return fixture
return decorator
indirect
关键字参数的官方文档确实很简略并且不友好,这可能解释了为什么这种基本技巧如此晦涩难懂。我已经在 py.test 网站上多次搜索这个特性,但一直找不到,只变得更加老旧和困惑。苦涩的地方便是持续集成。感谢 Stackoverflow。 - Cecil Currytest_tc1
变成了test_tc1[tester0]
。 - jjjindirect=True
将参数传递给所有被调用的 fixture,对吗?因为文档明确命名了 indirect 参数化的 fixtures,例如一个名为x
的 fixture:indirect=['x']
。 - winklerrrTrue
和False
也适用于indirect
关键字。 - winklerrr