如何参数化夹具和测试函数¶
pytest 支持在多个级别进行测试参数化
@pytest.mark.parametrize 允许在测试函数或类上定义多组参数和夹具。
pytest_generate_tests 允许定义自定义参数化方案或扩展。
@pytest.mark.parametrize
: 参数化测试函数¶
内置的 pytest.mark.parametrize 装饰器可以参数化测试函数的参数。以下是检查特定输入是否导致预期输出的测试函数的典型示例:
# content of test_expectation.py
import pytest
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
assert eval(test_input) == expected
在这里,@parametrize
装饰器定义了三个不同的 (test_input,expected)
元组,以便 test_eval
函数将依次使用它们运行三次。
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 3 items
test_expectation.py ..F [100%]
================================= FAILURES =================================
____________________________ test_eval[6*9-42] _____________________________
test_input = '6*9', expected = 42
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
> assert eval(test_input) == expected
E AssertionError: assert 54 == 42
E + where 54 = eval('6*9')
test_expectation.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_expectation.py::test_eval[6*9-42] - AssertionError: assert 54...
======================= 1 failed, 2 passed in 0.12s ========================
注意
参数值会原样传递给测试(不进行任何复制)。
例如,如果传递列表或字典作为参数值,并且测试用例代码对其进行了修改,则这些修改将反映在后续的测试用例调用中。
注意
pytest 默认会转义参数化中使用的任何非 ASCII Unicode 字符串字符,因为它有几个缺点。但是,如果想在参数化中使用 Unicode 字符串并让它们在终端中显示为原样(未转义),请在 pytest.ini
中使用此选项:
[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
但请记住,这可能会根据所使用的操作系统和当前安装的插件导致不必要的副作用甚至错误,因此请自行承担风险。
如本例所示,只有一对输入/输出值会导致简单测试函数失败。与测试函数参数一样,可以在回溯中看到 input
和 output
值。
请注意,也可以在类或模块上使用 parametrize 标记(参见如何使用属性标记测试函数),这将使用参数集调用多个函数,例如:
import pytest
@pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])
class TestClass:
def test_simple_case(self, n, expected):
assert n + 1 == expected
def test_weird_simple_case(self, n, expected):
assert (n * 1) + 1 == expected
要参数化模块中的所有测试,可以赋值给 pytestmark
全局变量:
import pytest
pytestmark = pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])
class TestClass:
def test_simple_case(self, n, expected):
assert n + 1 == expected
def test_weird_simple_case(self, n, expected):
assert (n * 1) + 1 == expected
还可以在 parametrize 中标记单个测试实例,例如使用内置的 mark.xfail
:
# content of test_expectation.py
import pytest
@pytest.mark.parametrize(
"test_input,expected",
[("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)],
)
def test_eval(test_input, expected):
assert eval(test_input) == expected
让我们运行一下:
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 3 items
test_expectation.py ..x [100%]
======================= 2 passed, 1 xfailed in 0.12s =======================
之前导致失败的那个参数集现在显示为“xfailed”(预期失败)测试。
如果提供给 parametrize
的值导致空列表——例如,如果它们由某个函数动态生成——pytest 的行为由 empty_parameter_set_mark
选项定义。
要获取多个参数化参数的所有组合,可以堆叠 parametrize
装饰器:
import pytest
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
pass
这将以 x=0/y=2
、x=1/y=2
、x=0/y=3
和 x=1/y=3
的参数集运行测试,按照装饰器的顺序耗尽参数。
pytest_generate_tests
基本示例¶
有时,您可能希望实现自己的参数化方案,或者为确定夹具的参数或作用域实现一些动态性。为此,可以使用 pytest_generate_tests
钩子,它在收集测试函数时被调用。通过传入的 metafunc
对象,可以检查请求的测试上下文,最重要的是,可以调用 metafunc.parametrize()
来触发参数化。
例如,假设我们想运行一个接受字符串输入的测试,我们希望通过一个新的 pytest
命令行选项来设置它。首先,我们编写一个接受 stringinput
夹具函数参数的简单测试:
# content of test_strings.py
def test_valid_string(stringinput):
assert stringinput.isalpha()
现在我们添加一个 conftest.py
文件,其中包含命令行选项的添加和我们测试函数的参数化:
# content of conftest.py
def pytest_addoption(parser):
parser.addoption(
"--stringinput",
action="append",
default=[],
help="list of stringinputs to pass to test functions",
)
def pytest_generate_tests(metafunc):
if "stringinput" in metafunc.fixturenames:
metafunc.parametrize("stringinput", metafunc.config.getoption("stringinput"))
如果现在传入两个 stringinput 值,我们的测试将运行两次:
$ pytest -q --stringinput="hello" --stringinput="world" test_strings.py
.. [100%]
2 passed in 0.12s
我们还运行一个将导致测试失败的 stringinput:
$ pytest -q --stringinput="!" test_strings.py
F [100%]
================================= FAILURES =================================
___________________________ test_valid_string[!] ___________________________
stringinput = '!'
def test_valid_string(stringinput):
> assert stringinput.isalpha()
E AssertionError: assert False
E + where False = <built-in method isalpha of str object at 0xdeadbeef0001>()
E + where <built-in method isalpha of str object at 0xdeadbeef0001> = '!'.isalpha
test_strings.py:4: AssertionError
========================= short test summary info ==========================
FAILED test_strings.py::test_valid_string[!] - AssertionError: assert False
1 failed in 0.12s
正如预期的那样,我们的测试函数失败了。
如果未指定 stringinput,它将被跳过,因为 metafunc.parametrize()
将被空参数列表调用:
$ pytest -q -rs test_strings.py
s [100%]
========================= short test summary info ==========================
SKIPPED [1] test_strings.py: got empty parameter set for (stringinput)
1 skipped in 0.12s
请注意,当使用不同的参数集多次调用 metafunc.parametrize
时,这些参数集中的所有参数名称不能重复,否则会引发错误。
更多示例¶
有关更多示例,您可能需要查看更多参数化示例。