python 之 pytest 使用参数化夹具测试(六)

如何使用参数化夹具测试

pytest 支持多个级别的测试参数化:

  • pytest.fixture() 允许对夹具函数进行参数化。
  • @pytest.mark.parametrize 允许在测试函数或类中定义多组参数和fixture。
  • pytest_generate_tests 允许定义自定义参数化方案或扩展。

参数化测试方法:@pytest.mark.parametrize

内置的 pytest.mark.parametrize 装饰器可以对测试函数的参数进行参数化。以下是测试函数的典型示例,该函数实现检查特定输入是否会产生预期输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 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测试函数将依次使用它们运行三次:
=================================== 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')

testcases/test_parametrize_case.py:6: AssertionError
=========================== short test summary info ============================
FAILED testcases/test_parametrize_case.py::test_eval[6*9-42] - AssertionError...
========================= 1 failed, 2 passed in 0.01s ==========================

参数值按原样传递给测试(没有任何副本)。—-> 引用传递 函数接收的是参数的引用或指针,即函数内部操作的是参数的引用,对参数的修改会影响到原始对象。

例如,如果您传递一个列表或字典作为参数值,并且测试用例代码对其进行变更,则这些变更将反映在后续测试用例调用中。

pytest 默认情况下会对 unicode 字符串中使用的任何非 ascii 字符进行转义以进行参数化,因为它有几个缺点。但是,如果您想在参数化中使用 unicode 字符串并按原样在终端中查看它们(非转义),请在 pytest.ini 中使用此选项:

1
2
3
[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
# 但请记住,这可能会导致不必要的副作用甚至错误,具体取决于所使用的操作系统和当前安装的插件,

正如本示例中所设计的,只有一组输入/输出值未通过 功能测试。与测试函数参数一样,可以在回溯中看到输入和输出值。

还可以在类或模块上使用mark.parametrize标记,类内的所有测试用例都可以使用这个参数化,例如:

1
2
3
4
5
6
7
8
9
10
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

要使参数化作用在该模块(.py文件)中的所有测试用例,可以定义一个pytestmark 全局变量去接收这个参数化:

1
2
3
4
5
6
7
8
9
10
11
import pytest

pytestmark = pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)]) # 使用pytestmark接收


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

还可以在参数化中标记单个测试实例,例如使用内置的 mark.xfail:(可能错误标记)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 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

# 运行结果========================
============================= test session starts ==============================
configfile: pytest.ini
plugins: anyio-4.2.0
collecting ... collected 3 items

testcases/test_parametrize_case.py::test_eval[6*9-42] XFAIL # 错误标记
testcases/test_parametrize_case.py::test_eval[2+4-6] PASSED
testcases/test_parametrize_case.py::test_eval[3+5-8] PASSED

========================= 2 passed, 1 xfailed in 0.01s =========================

之前导致失败的一个参数集现在显示为“xfailed”(预期失败)测试。

如果提供给参数化的值导致空列表 - 例如,如果它们是由某个函数动态生成的 - pytest 的行为由empty_parameter_set_mark 选项定义。

要获取多个参数化参数的所有组合,可以堆叠使用参数化装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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,按照装饰器的顺序开始参数。
============================= test session starts ==============================
plugins: anyio-4.2.0
collecting ... collected 4 items

testcases/test_parametrize_case.py::test_foo[3-0] x=0, y=3 # 先从第二个的最右边的开始运行
PASSED
testcases/test_parametrize_case.py::test_foo[3-1] x=1, y=3
PASSED
testcases/test_parametrize_case.py::test_foo[2-1] x=1, y=2
PASSED
testcases/test_parametrize_case.py::test_foo[2-0] x=0, y=2 # 最后再运行第一个的最左边的
PASSED

============================== 4 passed in 0.01s ===========

基本 pytest_generate_tests 示例

有时,您可能希望实现自己的参数化方案或实现一些动态来确定夹具的参数或范围。为此,您可以使用pytest_generate_tests钩子,该钩子在收集测试函数时被调用。通过传入的metafunc对象,您可以检查请求测试上下文,最重要的是,您可以调用metafunc. parameterize()来进行参数化。

例如,假设我们想要运行一个测试,获取我们想要通过新的 pytest 命令行选项设置的字符串输入。让我们首先编写一个接受字符串输入固定函数参数的简单测试:

1
2
3
4
# content of test_strings.py

def test_valid_string(stringinput):
assert stringinput.isalpha() # 以字母开头

然后在conftest.py 文件中添加,其中包含命令行选项和测试函数的参数化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 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"))

如果我们现在传递两个字符串输入值,我们的测试将运行两次:

1
2
3
$ pytest -q --stringinput="hello" --stringinput="world" test_strings.py
.. [100%]
2 passed in 0.12s

我们还使用一个字符串输入来运行,这将导致测试失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ pytest -q --stringinput="!" test_strings.py # stringinput=!
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

如果不指定字符串输入,它将被跳过,因为将使用空参数列表调用metafunc.parametrize():

1
2
3
4
5
$ pytest -q -rs test_strings.py
s [100%]
========================= short test summary info ==========================
SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at /home/sweet/project/test_strings.py:2
1 skipped in 0.12s # 提示被跳过

请注意,当使用不同的参数集多次调用metafunc.parametrize时,这些集中的所有参数名称不能重复,否则会引发错误。

更多示例

对于更多示例 可以查看更多参数化示例 more parametrization examples


python 之 pytest 使用参数化夹具测试(六)
http://example.com/2024/01/20/666python-之-pytest-使用参数化夹具测试(六)/
作者
Wangxiaowang
发布于
2024年1月20日
许可协议