在 Python 中动态参数化多个测试

问题描述

我正在尝试使用 Pytest 编写动态测试套件,其中测试数据保存在单独的文件中,例如YAML 文件或 .csv。我想运行多个测试,所有这些测试都是从同一个文件中参数化的。假设我有一个测试文件 test_foo.py,如下所示:

import pytest

@pytest.mark.parametrize("num1,num2,output",([2,2,4],[3,7,10],[48,52,100]))
def test_addnums(num1,output):
    assert foo.addnums(num1,num2) == output

@pytest.mark.parametrize("foo,bar",([1,2],['moo','mar'],[0.5,3.14]))
def test_foobar(foo,bar):
    assert type(foo) == type(bar)

使用参数化装饰器,我可以在 pytest 中运行多个测试,并且按预期工作:

test_foo.py::test_addnums[2-2-4] PASSED                                                                                                                                                            
test_foo.py::test_addnums[3-7-10] PASSED                                                                                                                                                           
test_foo.py::test_addnums[48-52-100] PASSED                                                                                                                                                        
test_foo.py::test_foobar[1-2] PASSED                                                                                                                                                               
test_foo.py::test_foobar[moo-mar] PASSED                                                                                                                                                           
test_foo.py::test_foobar[0.5-3.14] PASSED

但我想动态地参数化这些测试。我的意思是,我想在一个单独的文件中编写所有测试的测试数据,这样当我运行 pytest 时,它会将我编写的所有测试数据应用到每个测试函数中。假设我有一个类似于以下内容的 YAML 文件

test_addnums:
  params: [num1,output]
  values:
    - [2,4]
    - [3,10]
    - [48,100]

test_foobar:
  params: [foo,bar]
  values:
    - [1,2]
    - [moo,mar]
    - [0.5,3.14]

然后我想读取这个 YAML 文件并使用数据来参数化我的测试文件中的所有测试函数

我知道 pytest_generate_tests 钩子,我一直在尝试使用它来动态加载测试。我尝试将之前传递给 parametrize 装饰器的相同参数和数据值添加Metafunc.parametrize 钩子中:

def pytest_generate_tests(Metafunc):
    Metafunc.parametrize("num1,100]))
    Metafunc.parametrize("foo,3.14]))

def test_addnums(num1,num2) == output

def test_foobar(foo,bar):
    assert type(foo) == type(bar)

然而,这不起作用,因为 pytest 尝试将测试数据应用于每个函数

collected 0 items / 1 error                                           

=============================== ERRORS ================================
____________________ ERROR collecting test_foo.py _____________________
In test_addnums: function uses no argument 'foo'
======================= short test summary info =======================
ERROR test_foo.py
!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!
========================== 1 error in 0.16s ===========================

我想知道的是:如何使用 pytest 动态参数化多个测试?我已经使用 pdb 反省了 pytest,据我所知,Metafunc 只知道您在文件中定义的第一个测试。在我上面的例子中,test_addnums 首先被定义,所以当我在 pdb 调试器中打印 vars(Metafunc) 时,它显示这些值:

(Pdb) pp vars(Metafunc)
{'_arg2fixturedefs': {},'_calls': [<_pytest.python.CallSpec2 object at 0x7f4330b6e860>,<_pytest.python.CallSpec2 object at 0x7f4330b6e0b8>,<_pytest.python.CallSpec2 object at 0x7f4330b6e908>],'cls': None,'config': <_pytest.config.Config object at 0x7f43310dbdd8>,'deFinition': <FunctionDeFinition test_addnums>,'fixturenames': ['num1','num2','output'],'function': <function test_addnums at 0x7f4330b5a6a8>,'module': <module 'test_foo' from '<PATH>/test_foo.py'>}

但如果我切换 test_foobartest_addnums 函数,并颠倒 parametrize 调用的顺序,它会显示有关 test_foobar 的信息。

(Pdb) pp vars(Metafunc)
{'_arg2fixturedefs': {},'_calls': [<_pytest.python.CallSpec2 object at 0x7f6d20d5e828>,<_pytest.python.CallSpec2 object at 0x7f6d20d5e860>,<_pytest.python.CallSpec2 object at 0x7f6d20d5e898>],'config': <_pytest.config.Config object at 0x7f6d212cbd68>,'deFinition': <FunctionDeFinition test_foobar>,'fixturenames': ['foo','bar'],'function': <function test_foobar at 0x7f6d20d4a6a8>,'module': <module 'test_foo' from '<PATH>/test_foo.py'>}

所以看起来 Metafunc 实际上并没有在我的测试文件中存储关于每个测试函数的信息。因此我不能使用 fixturenamesfunction 属性,因为它们只适用于一个特定的功能,而不是所有功能

如果是这种情况,那么我如何访问所有其他测试函数并单独对它们进行参数化?

解决方法

您可以使用 pytest_generate_tests 执行此操作,正如您尝试过的,您只需为每个函数选择正确的参数进行参数化(为简单起见,我将解析 yaml 的结果放入全局 dict 中):

all_params = {
    "test_addnums": {
        "params": ["num1","num2","output"],"values":
            [
                [2,2,4],[3,7,10],[48,52,100]
            ]
    },"test_foobar":
        {
            "params": ["foo","bar"],"values": [
                [1,2],["moo","mar"],[0.5,3.14]
            ]
        }
}


def pytest_generate_tests(metafunc):
    fct_name = metafunc.function.__name__
    if fct_name in all_params:
        params = all_params[fct_name]
        metafunc.parametrize(params["params"],params["values"])


def test_addnums(num1,num2,output):
    assert num1 + num2 == output


def test_foobar(foo,bar):
    assert type(foo) == type(bar)

这是相关的输出:

$python -m pytest -v param_multiple_tests.py
...
collected 6 items

param_multiple_tests.py::test_addnums[2-2-4] PASSED
param_multiple_tests.py::test_addnums[3-7-10] PASSED
param_multiple_tests.py::test_addnums[48-52-100] PASSED
param_multiple_tests.py::test_foobar[1-2] PASSED
param_multiple_tests.py::test_foobar[moo-mar] PASSED
param_multiple_tests.py::test_foobar[0.5-3.14] PASSED
===================== 6 passed in 0.27s =======================

我认为您在文档中遗漏的是 pytest_generate_tests 分别为每个测试调用。更常见的使用方法是检查夹具名称而不是测试名称,例如:

def pytest_generate_tests(metafunc):
    if "foo" in metafunc.fixturenames and "bar" in metafunc.fixturenames:
         metafunc.parametrize(["foo",...)
,

我为此专门编写了一个名为 parametrize_from_file 的包。它通过提供一个装饰器来工作,该装饰器基本上与 @pytest.mark.parametrize 做同样的事情,只是它从外部文件读取参数。我认为这种方法比乱用 pytest_generate_tests 简单得多。

以下是它如何查找您在上面提供的示例数据。首先,我们需要重新组织数据,使得顶层是一个以测试名称为键的字典,第二层是测试用例列表,第三层是参数名称到参数值的字典:

test_addnums:
  - num1: 2
    num2: 2
    output: 4

  - num1: 3
    num2: 7
    output: 10

  - num1: 48
    num2: 52
    output: 100

test_foobar:
  - foo: 1
    bar: 2

  - foo: boo
    bar: mar

  - foo: 0.5
    bar: 3.14

接下来,我们只需要将 @parametrize_from_file 装饰器应用于测试:

import parametrize_from_file

@parametrize_from_file
def test_addnums(num1,output):
    assert foo.addnums(num1,num2) == output

@parametrize_from_file
def test_foobar(foo,bar):
    assert type(foo) == type(bar)

这里假设 @parameterize_from_file 能够在默认位置找到参数文件,这是一个与测试脚本具有相同基本名称的文件(例如 test_things.{yml,toml,nt} 代表 test_things.py) .但您也可以手动指定路径。

parametrize_from_file 的其他一些值得简要提及的功能,但通过 pytest_generate_tests 实现自己会很烦人:

  • 您可以在每个测试用例的基础上指定 ID 和标记。
  • 您可以将架构应用于测试用例。我经常用它来 eval 段 Python 代码。
  • 您可以在同一个测试函数上多次使用 @parametrize_from_file@pytest.mark.parametrize
  • 如果有关参数文件的任何内容没有意义(例如,错误的组织、缺少名称、不一致的参数集等),您将收到很好的错误消息