如何参数化 fixture 和测试函数

pytest 支持在多个层次上参数化测试:

– pytest.fixture() 允许参数化 fixture 函数。

– @pytest.mark.parametrize 允许在测试函数或类上定义多组实参与 fixture。

– 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-9.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 默认对参数化所用 Unicode 字符串中的非 ASCII 字符转义,因为直接显示这些字符存在一些不利影响。如果希望在终端中原样显示 Unicode 字符,可在配置文件中使用以下选项:

pyproject.toml:

[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = true

pytest.ini:

[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = true

但请记住,这可能因操作系统和已安装插件而产生不希望出现的副作用,甚至导致 bug。使用时需自行权衡。

按本例的设计,只有一组输入与输出使这个简单测试函数失败。和普通的测试函数实参一样,你可以在回溯信息中看到输入与输出值。

也可以在类或模块上使用 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-9.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 的基本示例

有时你可能想实现自己的参数化方案,或动态决定 fixture 的参数或作用域。为此,可以使用收集测试函数时调用的 pytest_generate_tests 钩子。通过传入的 metafunc 对象,你可以检查请求测试的上下文,尤其可以调用 metafunc.parametrize() 实现参数化。

例如,我们希望运行接收字符串输入的测试,并通过新增的 pytest 命令行选项设置这些输入。先编写一个简单测试,接收名为 stringinput 的 fixture 函数实参:

# 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_generate_tests 钩子还可以直接在测试模块或测试类中实现;与其他钩子不同,pytest 也会在那里发现它。其他钩子必须放在 conftest.py 或插件中。参见编写钩子函数。

现在传入两个 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 并传入不同参数组时,各组之间不能出现重复的参数名,否则会引发错误。

更多示例

更多示例参见其他参数化示例。

—

来源:pytest:How to parametrize fixtures and test functions。© 2015 holger krekel and pytest-dev team。正文为中文翻译,代码、参数名及终端输出保留原文。原文测试输出中的耗时是示例结果。

MIT License

Copyright (c) 2004 Holger Krekel and others

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容