如何将参数传递给 Pytest 中的夹具函数?
- 2025-02-28 08:22:00
- admin 原创
- 67
问题描述:
我正在使用 py.test 测试一些包装在 Python 类 MyTester 中的 DLL 代码。为了验证目的,我需要在测试期间记录一些测试数据,然后进行更多处理。由于我有许多 test_... 文件,我想在大多数测试中重用测试器对象创建(MyTester 的实例)。
由于测试器对象是获取 DLL 变量和函数引用的对象,因此我需要将 DLL 变量列表传递给每个测试文件的测试器对象(对于 test_... 文件,要记录的变量是相同的)。列表的内容用于记录指定的数据。
我的想法是以这样的方式实现的:
import pytest
class MyTester():
def __init__(self, arg = ["var0", "var1"]):
self.arg = arg
# self.use_arg_to_init_logging_part()
def dothis(self):
print "this"
def dothat(self):
print "that"
# located in conftest.py (because other test will reuse it)
@pytest.fixture()
def tester(request):
""" create tester object """
# how to use the list below for arg?
_tester = MyTester()
return _tester
# located in test_...py
# @pytest.mark.usefixtures("tester")
class TestIt():
# def __init__(self):
# self.args_for_tester = ["var1", "var2"]
# # how to pass this list to the tester fixture?
def test_tc1(self, tester):
tester.dothis()
assert 0 # for demo purpose
def test_tc2(self, tester):
tester.dothat()
assert 0 # for demo purpose
是否可以这样实现或者是否有更优雅的方法?
通常我可以使用某种设置函数(xUnit 样式)对每种测试方法执行此操作。但我想获得某种重用。有人知道这是否可以通过装置实现吗?
我知道我可以做这样的事情:(来自文档)
@pytest.fixture(scope="module", params=["merlinux.eu", "mail.python.org"])
但是我需要直接在测试模块中进行参数化。
是否可以从测试模块访问夹具的 params 属性?
解决方案 1:
这实际上是通过间接参数化在 py.test 中原生支持的。
就你的情况来说,你应该:
@pytest.fixture
def tester(request):
"""Create tester object"""
return MyTester(request.param)
class TestIt:
@pytest.mark.parametrize('tester', [['var1', 'var2']], indirect=True)
def test_tc1(self, tester):
tester.dothis()
assert 1
解决方案 2:
更新:由于这是该问题的可接受答案,并且有时仍会得到赞成,我应该添加更新。虽然我的原始答案(如下)是在旧版本的 pytest 中执行此操作的唯一方法,但其他人已经注意到pytest 现在支持夹具的间接参数化。例如,您可以执行如下操作(通过@imiric):
# test_parameterized_fixture.py
import pytest
class MyTester:
def __init__(self, x):
self.x = x
def dothis(self):
assert self.x
@pytest.fixture
def tester(request):
"""Create tester object"""
return MyTester(request.param)
class TestIt:
@pytest.mark.parametrize('tester', [True, False], indirect=['tester'])
def test_tc1(self, tester):
tester.dothis()
assert 1
$ pytest -v test_parameterized_fixture.py
================================================================================= test session starts =================================================================================
platform cygwin -- Python 3.6.8, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: .
collected 2 items
test_parameterized_fixture.py::TestIt::test_tc1[True] PASSED [ 50%]
test_parameterized_fixture.py::TestIt::test_tc1[False] FAILED
然而,尽管这种间接参数化形式是明确的,但正如@Yukihiko Shinoda指出的那样,它现在支持一种隐式间接参数化形式(尽管我在官方文档中找不到任何明显的参考):
# test_parameterized_fixture2.py
import pytest
class MyTester:
def __init__(self, x):
self.x = x
def dothis(self):
assert self.x
@pytest.fixture
def tester(tester_arg):
"""Create tester object"""
return MyTester(tester_arg)
class TestIt:
@pytest.mark.parametrize('tester_arg', [True, False])
def test_tc1(self, tester):
tester.dothis()
assert 1
$ pytest -v test_parameterized_fixture2.py
================================================================================= test session starts =================================================================================
platform cygwin -- Python 3.6.8, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: .
collected 2 items
test_parameterized_fixture2.py::TestIt::test_tc1[True] PASSED [ 50%]
test_parameterized_fixture2.py::TestIt::test_tc1[False] FAILED
我不清楚这种形式的语义到底是什么,但它似乎pytest.mark.parametrize
认识到,虽然该test_tc1
方法不采用名为的参数tester_arg
,但tester
它使用的装置却采用,因此它通过装置传递参数化参数tester
。
我遇到了类似的问题——我有一个名为的装置test_package
,后来我希望能够在特定测试中运行该装置时向其传递一个可选参数。例如:
@pytest.fixture()
def test_package(request, version='1.0'):
...
request.addfinalizer(fin)
...
return package
(对于这些目的来说,装置的作用或返回的对象类型并不重要package
)。
然后,最好以某种方式在测试函数中使用此装置,以便我还可以指定version
该装置的参数以用于该测试。目前这是不可能的,但可能会成为一个很好的功能。
与此同时,可以很容易地让我的装置简单地返回一个函数,该函数完成装置以前所做的所有工作,但允许我指定version
参数:
@pytest.fixture()
def test_package(request):
def make_test_package(version='1.0'):
...
request.addfinalizer(fin)
...
return test_package
return make_test_package
现在我可以在我的测试函数中使用它,例如:
def test_install_package(test_package):
package = test_package(version='1.1')
...
assert ...
等等。
OP 尝试的解决方案朝着正确的方向前进,并且正如@hpk42 的回答所暗示的那样,MyTester.__init__
可以存储对请求的引用,例如:
class MyTester(object):
def __init__(self, request, arg=["var0", "var1"]):
self.request = request
self.arg = arg
# self.use_arg_to_init_logging_part()
def dothis(self):
print "this"
def dothat(self):
print "that"
然后使用它来实现如下的装置:
@pytest.fixture()
def tester(request):
""" create tester object """
# how to use the list below for arg?
_tester = MyTester(request)
return _tester
如果需要的话,MyTester
可以稍微重构一下类,以便.args
在创建之后可以更新它的属性,以调整各个测试的行为。
解决方案 3:
正如 @chilicheech 指出的那样,至少从 pytest 6.2(2020-12-13 发布)开始,官方就记录了这种方式:
使用直接测试参数化覆盖装置 | 如何使用装置 — pytest 文档
pytest 文档版本 6.2
它似乎在最新版本的 pytest 中运行。
@pytest.fixture
def tester(tester_arg):
"""Create tester object"""
return MyTester(tester_arg)
class TestIt:
@pytest.mark.parametrize('tester_arg', [['var1', 'var2']])
def test_tc1(self, tester):
tester.dothis()
assert 1
解决方案 4:
您还可以使用闭包,这将为您提供更全面的参数命名和控制。它们比隐式参数化request
中使用的参数更“明确” :
@pytest.fixture
def tester():
# Create a closure on the Tester object
def _tester(first_param, second_param):
# use the above params to mock and instantiate things
return MyTester(first_param, second_param)
# Pass this closure to the test
yield _tester
@pytest.mark.parametrize(['param_one', 'param_two'], [(1,2), (1000,2000)])
def test_tc1(tester, param_one, param_two):
# run the closure now with the desired params
my_tester = tester(param_one, param_two)
# assert code here
我使用它来构建可配置的装置。
解决方案 5:
稍微改进一下imiric 的回答:解决这个问题的另一种优雅方法是创建“参数装置”。我个人更喜欢它而不是indirect
的功能pytest
。此功能可从 获得pytest_cases
,最初的想法是由Sup3rGeo提出的。
import pytest
from pytest_cases import param_fixture
# create a single parameter fixture
var = param_fixture("var", [['var1', 'var2']], ids=str)
@pytest.fixture
def tester(var):
"""Create tester object"""
return MyTester(var)
class TestIt:
def test_tc1(self, tester):
tester.dothis()
assert 1
请注意,pytest-cases
还提供了@fixture
允许您直接在灯具上使用参数化标记的功能,而不必使用@pytest.fixture(params=...)
from pytest_cases import fixture, parametrize
@fixture
@parametrize("var", [['var1', 'var2']], ids=str)
def tester(var):
"""Create tester object"""
return MyTester(var)
并且@parametrize_with_cases
允许您从可能分组在类中甚至单独模块中的“case 函数”中获取参数。请参阅文档了解详情。顺便说一下,我是作者 ;)
解决方案 6:
您可以从装置函数(以及您的 Tester 类)访问请求模块/类/函数,请参阅从装置函数与请求测试上下文交互。因此,您可以在类或模块上声明一些参数,然后测试器装置就可以获取它。
解决方案 7:
我制作了一个有趣的装饰器,可以编写如下的装置:
@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
,则它不会与此冲突。
还支持异步装置。
此外,这还为您提供了一个很好的设置计划:
$ 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
解决方案 8:
另一种方法是使用请求对象来访问测试函数所定义的模块或类中定义的变量。
@pytest.mark.parametrize()
这样,如果您想为类/模块的所有测试函数传递相同的变量,则不必在测试类的每个函数上重用装饰器。
类变量的示例:
@pytest.fixture
def tester(request):
"""Create tester object"""
return MyTester(request.cls.tester_args)
class TestIt:
tester_args = ['var1', 'var2']
def test_tc1(self, tester):
tester.dothis()
def test_tc2(self, tester):
tester.dothat()
这样,tester
test_tc1 和 test_tc2 的对象都将使用参数进行初始化tester_args
。
您还可以使用:
request.function
访问 test_tc1 函数,request.instance
访问 TestIt 类实例,request.module
访问模块 TestIt 定义在等等(参考
request
文档)
解决方案 9:
另一种方法是使用自定义标记。它在代码中看起来比参数化更好,不会反映在测试名称中,也是可选的(如果不存在这样的标记,可以通过引发失败来定义为非可选)
例如:
@pytest.fixture
def loaded_dll(request):
dll_file = None
for mark in request.node.iter_markers("dll_file"):
if mark.args:
if dll_file is not None:
pytest.fail("Only one dll_file can be mentioned in marks")
dll_file = mark.args[0]
if dll_file is None:
pytest.fail("dll_file is a required mark")
return some_dll_load(dll_file)
@pytest.mark.dll_file("this.dll")
def test_this_dll(loaded_dll):
...
当我需要一个模拟 ssh 客户端的装置并想要测试不同的可能输出时,我将它用于我的测试,我可以使用标记来决定每个测试的输出。
请注意,如果它是供个人使用,则不需要未通过测试的故障保存机制。
解决方案 10:
Ori Markovitch 的回答无疑是最适合我的情况的技巧。唯一的缺点是 Fixture 不会显示为参数化测试用例,但在大多数情况下我认为这没问题。就我而言,我希望测试用例上方的所有测试参数都包含在这个技巧中。我只想指出,使用 Fixture 标记技巧,您既可以传递 args 和 kwargs,也可以指定拆分为不同标记的多个参数。在错误处理方面,您还需要做一些事情,但对于我制作的示例,我保持简单。
我在这里举了几个例子来展示如何利用这一点:
import pytest
from _pytest.mark import Mark
def get_marker(request, name) -> Mark:
markers = list(request.node.iter_markers(name))
if len(markers) > 1:
pytest.fail(f"Found multiple markers for {name}")
return markers[0]
class FixMark:
def __init__(self, var1, var2):
self.var1 = var1
self.var2 = var2
@pytest.fixture
def var_test(request):
mark1 = get_marker(request, "var_test_var1")
mark2 = get_marker(request, "var_test_var2")
var1 = mark1.kwargs["var1"] if len(mark1.kwargs) else mark1.args[0]
var2 = mark2.kwargs["var2"] if len(mark2.kwargs) else mark2.args[0]
yield FixMark(var1, var2)
@pytest.fixture
def var_combined_test(request):
mark = get_marker(request, "var_combined")
var1 = mark.kwargs["var1"] if mark.kwargs.get("var1") else mark.args[0]
var2 = mark.kwargs["var2"] if mark.kwargs.get("var2") else mark.args[1]
yield FixMark(var1, var2)
@pytest.mark.var_test_var1("ABC")
@pytest.mark.var_test_var2("DEF")
def test_var_test_args(var_test):
assert var_test.var1 == "ABC"
assert var_test.var2 == "DEF"
@pytest.mark.var_test_var1(var1="ABC")
@pytest.mark.var_test_var2(var2="DEF")
def test_var_test_kwargs(var_test):
assert var_test.var1 == "ABC"
assert var_test.var2 == "DEF"
@pytest.mark.var_combined("ABC", "DEF")
def test_vars_combined_args(var_combined_test):
assert var_combined_test.var1 == "ABC"
assert var_combined_test.var2 == "DEF"
@pytest.mark.var_combined(var1="ABC", var2="DEF")
def test_vars_combined_kwargs(var_combined_test):
assert var_combined_test.var1 == "ABC"
assert var_combined_test.var2 == "DEF"
解决方案 11:
Fixture 的工作原理类似于装饰器。我认为它更简单、更清晰。你也可以使用它
在 conftest.py 中
@pytest.fixture
def tester():
def wrapper(arg):
_tester = MyTester(arg)
return _tester
return wrapper
在 test.py 中
class TestIt():
def test_tc1(self, tester, arg): # test function takes fixture and arg
mock_tester = tester(arg) # mock_tester just an instance of MyTester
mock_tester.dothis() # so you get instance with initialized arg
assert 0 # for demo purpose
解决方案 12:
以下是使用装置中的命名参数执行此操作的方法,并附带 mypy 类型支持。请注意tuple
参数化装饰器中使用的值,它允许您分离参数 - 为参数化测试用例向 param 数组中添加任意数量的元组。
import os
from typing import Literal
import pytest
@pytest.fixture(autouse=False, scope="function")
def set_env(key: str, value: str):
"""Set an environment variable temporarily, backing up and restoring the original value if it was set before the test was run."""
orig_value = os.getenv(key)
os.environ[key] = value
yield
if orig_value is not None:
os.environ[key] = orig_value
else:
del os.environ[key]
@pytest.mark.parametrize("key, value", [("TEST_ENV", "env_value")])
def test_get_env(set_env: Literal["TEST_ENV"] | Literal["env_value"]):
assert os.getenv("TEST_ENV") == "env_value"
您还可以附加额外的标准参数化装置或变量 - 并且 pytest 知道(神奇地?)计算装置函数中预期参数的数量,并将其余的参数作为标准参数传递给测试:
@pytest.mark.parametrize(
"key, value, default, expected, expected_default",
[
("TEST_ENV", "env_value", None, "env_value", None),
("TEST_ENV", "env_value", "default_value", "env_value", "default_value"),
],
)
def test_get_env(
set_env: None,
default: str | None,
expected: str,
expected_default: str | None,
):
assert os.getenv("TEST_ENV") == expected
assert os.environ.get("TEST_ENV") == expected
assert os.environ.get("NOT_SET_ENV", default) == expected_default
解决方案 13:
例如,使用@pytest.fixture(params=(...)),您可以将Apple
、Orange
和Banana
传递给fruits()
夹具本身,然后传递给 ,test()
如下所示:
import pytest
@pytest.fixture(params=("Apple", "Orange", "Banana"))
def fruits(request):
print("fruits", request.param) # 1st
return request.param
def test(fruits):
print("test", fruits) # 2nd
assert True
输出:
$ pytest -q -rP
... [100%]
=============== PASSES ================
_____________ test[Apple] _____________
-------- Captured stdout setup --------
fruits Apple
-------- Captured stdout call ---------
test Apple
____________ test[Orange] _____________
-------- Captured stdout setup --------
fruits Orange
-------- Captured stdout call ---------
test Orange
____________ test[Banana] _____________
-------- Captured stdout setup --------
fruits Banana
-------- Captured stdout call ---------
test Banana
3 passed in 0.11s
举例来说,你可以向addition()
具有嵌套函数的 Fixture传递 2 个参数core()
,如下所示:
import pytest
@pytest.fixture
def addition():
def core(num1, num2):
return num1 + num2
return core
def test(request, addition):
print(addition(2, 3))
print(request.getfixturevalue("addition")(6, 8))
assert True
输出:
$ pytest -q -rP
. [100%]
=============== PASSES ================
________________ test _________________
-------- Captured stdout call ---------
5
14
1 passed in 0.10s
解决方案 14:
在我的例子中,我需要在夹具拆卸时根据其 ID 删除请求。问题是只有在测试期间我才能获取我的请求 ID。所以我需要一个夹具,它可以在测试中间接受一个参数,并在测试后删除我的请求。我想出了这样的东西:
@pytest.fixture()
def delete_request():
# setUp:
local_request_id = None
def get_request_id_from_test(request_id: int):
nonlocal local_request_id
local_request_id = request_id
yield get_request_id_from_test
# tearDown:
api.delete_request(local_request_id)
def test_create_request(delete_request):
# Array
...
# Act
... # here i get request id after creating a request
delete_request(request_id)
...
此决定不需要任何标记和参数化
扫码咨询,免费领取项目管理大礼包!