在 Python 中动态参数化多个测试
Parametrizing multiple tests dynamically in 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, 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
,因此当我在 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>],
'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 实际上并没有在我的测试文件中存储关于每个测试函数的信息。因此我不能使用 fixturenames
或 function
属性,因为它们只适用于一个特定的功能,而不是所有功能。
如果是这样,那么我怎样才能访问所有其他测试函数并分别对它们进行参数化?
你可以使用 pytest_generate_tests
来做到这一点,正如你所尝试的那样,你只需要 select 为每个函数的参数化设置正确的参数(我将解析 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"], ...)
我为此编写了一个名为 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 和标记。
- 您可以将架构应用于测试用例。我经常用它来
eval
python 代码片段。
- 您可以在同一个测试函数上多次使用
@parametrize_from_file
和 @pytest.mark.parametrize
。
- 如果参数文件有任何不合理之处(例如组织错误、名称缺失、参数集不一致等),您将收到正确的错误消息
我正在尝试使用 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
,因此当我在 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>],
'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 实际上并没有在我的测试文件中存储关于每个测试函数的信息。因此我不能使用 fixturenames
或 function
属性,因为它们只适用于一个特定的功能,而不是所有功能。
如果是这样,那么我怎样才能访问所有其他测试函数并分别对它们进行参数化?
你可以使用 pytest_generate_tests
来做到这一点,正如你所尝试的那样,你只需要 select 为每个函数的参数化设置正确的参数(我将解析 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"], ...)
我为此编写了一个名为 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 和标记。
- 您可以将架构应用于测试用例。我经常用它来
eval
python 代码片段。 - 您可以在同一个测试函数上多次使用
@parametrize_from_file
和@pytest.mark.parametrize
。 - 如果参数文件有任何不合理之处(例如组织错误、名称缺失、参数集不一致等),您将收到正确的错误消息