参数化的夹具和测试
pytest在多个级别上都可以做参数化:
- pytest.fixture() 允许用户参数化夹具函数
- @pytest.mark.parametrize 允许用户给测试方法或者类定义多组参数或者多个夹具
- pytest_generate_tests允许用户定义自定义的参数格式或者扩展
13.1 @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-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
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会使用unicode字符串转义所有的非unicode参数,因为非unicode会导致一些问题。如果你就是想在参数中使用unicode字符串,并且在输出窗口也希望看到unicode(非转义的),你可以在pytest.ini中使用这个选项:
[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
记着这可能导致意料之外的副作用或者某些系统,或者安装了某些插件下会产生bug。使用它,你就需要自行承担风险。
在例子的设计中,只有一对输入/输出会产生失败。通常情况下,你可以在追踪信息中看到输入和输出值。
你也可以在类或者模块中使用 parametrize 标记(查看第六章,给测试函数给添加属性),它们里面可以包含很多拥有参数组的测试。
我们也可以给参数化的一组参数中的某一个独立的测试添加标记,例如内置的标记 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-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 3 items
test_expectation.py ..x
======================= 2 passed, 1 xfailed in 0.12s =======================
之前会引发一个失败的测试参数现在会显示一个 “xfailed” (expected to fail期望失败)。
如果给一个空的参数(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,直到耗尽参数的组合。
13.2 基本的 pytest_generate_tests 例子
一些情况下,你可能想要实现自己的参数化方案或为参数的确定或者夹具的scope确定提供动态改变的能力。这种情况,你可以使用pytest_generate_tests钩子,它会在收集测试的时候调用。通过传入的metafunc对象,您可以获取测试请求的上下文,最重要的是,您可以调用metafunc.parameterize()来做参数化。
例如,我们想写一个拥有字符串输入的测试,但是是通过命令行在运行的时候输入。我们先写一个简单的测试,这个测试接收一个名字叫 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"))
如果我们传入两个输入值进去,测试会执行两次:
$ pytest -q --stringinput="hello" --stringinput="world" test_strings.py
.. [100%]
2 passed in 0.12s
我们再运行一个会导致失败的测试:
$ 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 0xdeadbeef>()
E + where <built-in method isalpha of str object at 0xdeadbeef> = '!'.
˓→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 ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:2
1 skipped in 0.12s
注意如果使用不同的参数集合多次调用 metafunc.parametrize,这些参数集合不能重名,否则就会导致一个错误。
13.3 更多例子
你可以在 27.3 中获取更多参数化的例子。