问题描述
我正在尝试使用 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_foobar
和 test_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 实际上并没有在我的测试文件中存储关于每个测试函数的信息。因此我不能使用 fixturenames
或 function
属性,因为它们只适用于一个特定的功能,而不是所有功能。
如果是这种情况,那么我如何访问所有其他测试函数并单独对它们进行参数化?
解决方法
您可以使用 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
。 - 如果有关参数文件的任何内容没有意义(例如,错误的组织、缺少名称、不一致的参数集等),您将收到很好的错误消息