Pytest会在以下几个级别启用测试参数化:
- pytest.fixture(),可以对fixture函数进行参数化。
- @pytest.mark.parametrize,可以在测试函数或类中定义多组参数和fixture。
- pytest_generate_tests,可以自定义参数化方案或扩展。
一、@pytest.mark.parametrize:参数化测试函数
1. 常规用法
对测试函数的参数进行参数化,直接使用内置的装饰器pytest.mark.parameterized
即可。
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
从代码里可以看出,在装饰器里定义了三个不同的元组。我们把("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
拆开看:
- "test_input,expected":这个字符串里定义了2个参数,
test_input
和expected
。 - ("3+5", 8), ("2+4", 6), ("6*9", 42):这里3个元组,没个元组里有2个元素,依次序分别对应
test_input
和expected
。 - 3个元组外层的[]:列表里就是参数化具体的传参了,因为里面传了3个不同的元组,所以测试函数
test_eval
会分别执行3次。
============================= test session starts ============================= platform win32 -- Python 3.9.4, pytest-6.2.3, py-1.10.0, pluggy-0.13.1 rootdir: D:\PycharmProjects\wms-api\interface, configfile: pytest.inicollected 3 items test_module1.py ..F demo\test_module1.py:3 (test_eval[6*9-42]) 54 != 42 Expected :42 Actual :54 <Click to see difference> 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_module1.py:6: AssertionError
运行结果可以看到最后一次失败了,因为第三次运行测试函数取的参数是 ("6*9", 42)
,54不等于42,所以断言失败。
2. 在参数化中标记单个测试实例
在参数化中标记单个测试实例,比如之前提到过的mark.xfail
,这个可以标记测试函数为失败。那么在参数化中,如果想让其中的某个参数运行
的时候测试失败,就可以这样用:
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_module1.py [100%] ======================== 2 passed, 1 xfailed in 0.05s =========================..x
3. 多个参数化组合,笛卡尔积
如果在测试函数上加了多个参数化装饰器,那么得到的参数组合是一个笛卡尔积:
import pytest @pytest.mark.parametrize("x", [0, 1]) @pytest.mark.parametrize("y", [2, 3]) def test_foo(x, y): print("\nx:", x) print("y:", y)
应该会组合成4组数据x=0/y=2
, x=1/y=2
, x=0/y=3
, 和x=1/y=3
,测试函数执行4次:
test_module1.py . x: 0 y: 2 . x: 1 y: 2 . x: 0 y: 3 . x: 1 y: 3 [100%] ============================== 4 passed in 0.01s ==============================
二、用钩子函数pytest_generate_tests example拓展
如果有些场景需要动态的确定参数或者fixture的使用范围,那么可以使用pytest_generate_tests
这个钩子函数,该函数会在收集测试函数时候被调用。
通过传入的metafunc
对象,可以检查请求测试函数的上下文,还可以进一步的调用metafunc.parameterize()
来实现参数化。
举例,有个测试函数需要接受输入的字符串作为参数,而且通过pytest命令行获取到,那么就要编写一个获取参数的fixture函数来给测试函数调用。
# content of test_strings.py def test_valid_string(stringinput): assert stringinput.isalpha()
新建conftest.py文件,fixture函数写在这里:
# 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"))
现在用命令行方式来运行这个测试函数:pytest -q --stringinput="hello" --stringinput="world" test_strings.py
,会运行2次。
D:\PycharmProjects\wms-api\interface\demo>pytest -q --stringinput="hello" --stringinput="world" test_strings.py .. [100%] 2 passed in 0.01s
再换个输入参数,让测试函数失败:pytest -q --stringinput="!" test_strings.py
FAILED test_strings.py::test_valid_string[!] - AssertionError: assert False 1 failed in 0.04s
如果没有字符串输入,那么测试函数它将被跳过。因为metafunc.parameterize()
被调用时,传过去的是一个列表:pytest -q -rs test_strings.py
SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:2 1 skipped in 0.12s
注意,在调用metafunc
时, 如果使用不同的参数集进行多次参数化,这些参数集上的所有参数名称都不能重复,否则将会报错。
总结:文中讲到的3种用法,实际应用中第一种最常见,第二种次之,至于第三种,可以作为一些用法启发。