2

我正在尝试使用 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, num2, 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, 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]

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

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

def pytest_generate_tests(metafunc):
    metafunc.parametrize("num1, num2, output", ([2, 2, 4], [3, 7, 10], [48, 52, 100]))
    metafunc.parametrize("foo, bar", ([1, 2], ['moo', 'mar'], [0.5, 3.14]))

def test_addnums(num1, num2, output):
    assert foo.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首先定义,所以当我vars(metafunc)在 pdb 调试器中打印时,它会显示这些值:

(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_foobarandtest_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>],
 'cls': None,
 '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 实际上并没有在我的测试文件中存储关于每个测试函数的信息。因此我不能使用fixturenamesorfunction属性,因为它们只适用于一个特定的功能,而不是全部。

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

4

2 回答 2

3

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, num2, 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}for test_things.py)。但您也可以手动指定路径。

其中一些其他功能parametrize_from_file值得一提,通过以下方式实现自己会很烦人pytest_generate_tests

  • 您可以在每个测试用例的基础上指定 id 和标记。
  • 您可以将模式应用于测试用例。我经常将它用于evalpython代码片段。
  • 您可以在同一测试功能上同时使用这两种方法@parametrize_from_file,也可以@pytest.mark.parametrize多次使用。
  • 如果有关参数文件的任何内容没有意义(例如错误的组织、缺少名称、不一致的参数集等),您将收到很好的错误消息
于 2021-04-01T23:02:23.270 回答
2

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

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", "bar"], ...)
于 2021-04-01T17:57:45.573 回答