Bootstrap

pytest8.x版本 中文使用文档-------6.如何参数化夹具和测试函数

目录

@pytest.mark.parametrize: 测试函数的参数化

基础的 pytest_generate_tests 示例


pytest 支持在多个级别上进行测试参数化:

  1. pytest.fixture()允许对夹具函数进行参数化(parametrize fixture functions)。

  2. @pytest.mark.parametrize允许在测试函数或类上定义多组参数和夹具。

  3. pytest_generate_tests允许定义自定义参数化方案或扩展。

@pytest.mark.parametrize: 测试函数的参数化

内置的 pytest.mark.parametrize 装饰器允许你为测试函数的参数进行参数化。以下是一个典型示例,展示了测试函数如何通过实现检查特定输入是否导致预期输出来进行参数化:

# 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

@pytest.mark.parametrize 装饰器接收一个包含元组的列表作为参数,每个元组代表一组测试输入(test_input)和预期输出(expected)。装饰器会为列表中的每一组值运行一次 test_eval 测试函数,每次调用时都会将元组中的值分别赋值给 test_inputexpected 变量。

这样,test_eval 函数会针对列表中的每组 ("test_input", "expected") 值运行一次,每次都会评估 test_input(使用 Python 的 eval() 函数,尽管在实践中应谨慎使用以避免安全风险),并断言其结果是否与 expected 相等。

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.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 字符串,并在终端中直接看到它们(不进行转义),可以在你的 pytest.ini 文件中使用以下选项:

[pytest]  
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True

但是,请记住,这可能会根据你使用的操作系统和当前安装的插件产生不希望的副作用甚至错误,因此请自行承担风险使用。

按照本例中的设计,只有一对输入/输出值未能通过简单的测试函数。与测试函数参数一样,你可以在回溯信息中看到输入和输出值。

请注意,你也可以在类或模块上使用 parametrize 标记(参见“如何使用属性标记测试函数”How to mark test functions with attributes),这将会使用参数集调用多个函数,例如:

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

TestClass 类使用了 pytestparametrize 装饰器来参数化测试方法。这意味着 test_simple_casetest_weird_simple_case 这两个测试方法都会针对 @pytest.mark.parametrize 提供的每一组 (n, expected) 参数值执行一次。

要在模块中参数化所有测试,您可以将 pytestmark 全局变量设置为一个 pytest.mark.parametrize 标记。

import pytest

# 将pytestmark全局变量设置为一个parametrize标记,该标记会应用于模块中的所有测试  
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

pytestmark 的设置使得 TestClass 中的两个测试方法 test_simple_casetest_weird_simple_case 都会接收到来自 pytest.mark.parametrize(n, expected) 参数集,并且针对每一组参数分别执行一次。但是,请注意,如果模块中还有其他不在类中的测试函数,它们也会接收到这些参数,除非它们显式地不接受这些额外的参数。

此外,虽然这种方法可以工作,但它可能会使测试的组织和意图变得不太清晰,特别是当模块中包含多个测试类时。

pytest 中,您也可以在参数化时单独标记测试实例,例如使用内置的 mark.xfail 来标记某些预期会失败的测试用例。

# test_expectation.py 文件的内容  
import pytest  
  
# 使用 @pytest.mark.parametrize 来参数化测试,同时标记特定的测试实例  
@pytest.mark.parametrize(  
    "test_input,expected",  # 参数化测试的输入和期望输出  
    [  
        ("3+5", 8),         # 第一个测试实例,预期通过  
        ("2+4", 6),         # 第二个测试实例,预期通过  
        # 第三个测试实例,预期失败,使用 pytest.param 来明确指定参数,并通过 marks 参数添加 xfail 标记  
        pytest.param("6*9", 42, marks=pytest.mark.xfail),  
    ],  
)  
def test_eval(test_input, expected):  
    # 使用 eval 函数计算 test_input 的结果,并断言该结果是否等于 expected  
    assert eval(test_input) == expected

test_eval 函数被参数化为三个测试实例。前两个实例 "3+5", 8"2+4", 6 预期会成功通过,因为 eval("3+5") 确实等于 8eval("2+4") 确实等于 6。第三个实例 "6*9", 42 使用了 pytest.param 来明确指定参数,并通过 marks=pytest.mark.xfail 将其标记为预期失败(xfail)。这意味着即使这个测试用例失败了,pytest 也会将其视为“预期失败”而不是真正的失败,从而不会影响测试的总体结果(除非您配置了 pytest 来严格对待 xfail)。

使用 pytest.parammarks 参数为参数化测试中的特定实例添加标记,是一种灵活的方式来控制测试的期望行为,特别是当您想要明确指出某些测试用例是已知的问题或尚未实现的特性时。

让我们运行一下:

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.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() #isalpha() 是 Python 中的一个字符串方法,用于检查字符串是否只包含字母

现在,我们添加一个 conftest.py 文件,该文件包含添加命令行选项以及测试函数的参数化:

# content of conftest.py

def pytest_addoption(parser):  
    parser.addoption(  
        "--stringinput",  
        action="append",  
        default=[],  
        help="传递给测试函数的字符串输入列表",  
    )  
  
def pytest_generate_tests(metafunc):
    if "stringinput" in metafunc.fixturenames:
        metafunc.parametrize("stringinput", metafunc.config.getoption("stringinput"))

在这个 conftest.py 文件中,我们做了以下事情:

  1. 使用 pytest_addoption 钩子添加了一个名为 --stringinput 的命令行选项。这个选项使用 action="append",这意味着用户可以在命令行中多次指定这个选项,并且所有值都会被收集到一个列表中。default=[] 表示如果没有提供 --stringinput 选项,则默认值为空列表。

  2. 使用 pytest_generate_tests 钩子来根据 --stringinput 选项提供的值参数化测试函数。在这个例子中,我们假设用户想要将每个字符串输入作为独立的测试案例。因此,我们通过遍历 stringinputs 列表(这是通过 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,它将被跳过,因为metafunc. 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.parametrize 时,这些集合中所有参数名称不能重复,否则将引发错误。

注:

pytest_addoption(parser) 钩子函数中,parser 是一个 argparse.ArgumentParser 类的实例 是 Python 标准库中的一个模块,用于编写用户友好的命令行接口。在 pytest 的上下文中,pytest_addoption 钩子允许你向 pytest 的命令行接口添加自定义的选项。

parser 对象提供了多种方法来定义这些选项,包括 add_argument()addoption()(后者实际上是 pytest 特有的包装器,用于简化添加与 pytest 相关的选项的过程)。然而,在 pytest_addoption 钩子中,通常会直接使用 parser.addoption() 方法,因为它已经为 pytest 做了适当的配置。

parser.addoption() 方法允许你定义一个新的命令行选项,包括其名称、类型、是否必须提供值、默认值、帮助信息等。这个方法会自动将这些选项添加到 pytest 的命令行接口中,并且你可以在测试期间通过 pytest 的配置对象(config)来访问这些选项的值。

metafunc.config.getoption() 是一种方式来获取通过 pytest 命令行接口(CLI)传递的自定义选项的值。这里的 metafunc 是一个特殊的对象,它在 pytest_generate_tests 钩子函数中可用,用于控制测试函数的参数化。然而,需要注意的是,在大多数测试函数中,你不会直接使用 metafunc,而是可能会使用 request 对象来获取配置选项,但在参数化测试或特定钩子函数中,metafunc 提供了对测试函数元数据的访问,包括配置选项。

metafunc.config.getoption() 的工作方式如下:

  • metafunc:这是一个特殊的函数参数,仅在 pytest_generate_tests 钩子函数中可用。它提供了对当前测试函数元数据的访问,比如它的名称、参数以及通过 pytest 命令行传递的配置选项。

  • configmetafunc.config 是一个 pytest.config.Config 对象的实例,它包含了 pytest 的配置信息,包括通过命令行传递的选项。

  • getoption()config.getoption(name) 是一个方法,用于获取名为 name 的命令行选项的值。这个名称应该是你通过 pytest_addoption 钩子添加到 pytest 命令行接口的选项名称(不包括前导的 --)。

更多示例

为了获得更多的示例,您可能想要查看更多的参数化示例(more parametrization examples.)。

;