如何使用参数化夹具测试 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 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 =================================== 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.01 s ==========================
参数值按原样传递给测试(没有任何副本)。—-> 引用传递 函数接收的是参数的引用或指针,即函数内部操作的是参数的引用,对参数的修改会影响到原始对象。
例如,如果您传递一个列表或字典作为参数值,并且测试用例代码对其进行变更,则这些变更将反映在后续测试用例调用中。
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 )]) 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 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.01 s =========================
之前导致失败的一个参数集现在显示为“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 ============================= 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.01 s ===========
基本 pytest_generate_tests 示例 有时,您可能希望实现自己的参数化方案或实现一些动态来确定夹具的参数或范围。为此,您可以使用pytest_generate_tests钩子,该钩子在收集测试函数时被调用。通过传入的metafunc对象,您可以检查请求测试上下文,最重要的是,您可以调用metafunc. parameterize()来进行参数化。
例如,假设我们想要运行一个测试,获取我们想要通过新的 pytest 命令行选项设置的字符串输入。让我们首先编写一个接受字符串输入固定函数参数的简单测试:
1 2 3 4 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 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.12 s
我们还使用一个字符串输入来运行,这将导致测试失败:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ 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.12 s
如果不指定字符串输入,它将被跳过,因为将使用空参数列表调用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.12 s
请注意,当使用不同的参数集多次调用metafunc.parametrize时,这些集中的所有参数名称不能重复,否则会引发错误。
更多示例
对于更多示例 可以查看更多参数化示例 more parametrization examples