pytest 核心功能-参数化fixtures和测试函数

2022-03-21 09:41 更新

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 ​函数将依次使用它们运行 3 次:

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.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 ========================

注意:参数值按原样传递给测试(没有任何副本)。

例如,如果您将一个列表或dict作为参数值传递,并且测试用例代码对它进行了更改,那么这些更改将在随后的测试用例调用中反映出来。

Pytest默认情况下转义用于参数化的unicode字符串中的任何非ascii字符,因为它有几个缺点。但是,如果你想在参数化中使用unicode字符串,并在终端中看到它们是(非转义的),在你的​pytest.ini​中使用这个选项:

[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True

但请记住,根据所使用的操作系统和当前安装的插件,这可能会导致不必要的副作用,甚至bug,所以使用它的风险由您自己承担。

正如本例中所设计的,只有一对输入/输出值不能通过简单的测试函数。和通常的测试函数参数一样,您可以在回溯中看到输入和输出值。

注意,你也可以在类或模块上使用参数化标记,它会调用几个带有参数集的函数,例如:

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

也可以在参数化中标记单个测试实例,例如使用内置的​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-7.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​(预期失败)测试。

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

要获得多个参数化参数的所有组合,你可以堆栈参数化装饰器:

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.parameterize()​来进行参数化。

假设我们想运行一个测试,接受我们想通过一个新的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"))

如果我们现在传递两个​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​,它将被跳过,因为​metafuncc.parameterize()​将被调用,参数列表为空:

$ 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​使用不同的参数集进行多次参数化,这些参数集中的所有参数名不能重复,否则将引发错误。


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号