参数化测试取决于pytest中的参数化值



我有一个测试,其中我有一种设置方法,它应该接收数据集,还有一个测试函数,它应该为中的每个数据运行

基本上我需要这样的东西:

datasetA = [data1_a, data2_a, data3_a]
datasetB = [data1_b, data2_b, data3_b]
@pytest.fixture(autouse=True, scope="module", params=[datasetA, datasetB])
def setup(dataset):
#do setup
yield
#finalize
#dataset should be the same instantiated for the setup
@pytest.mark.parametrize('data', [data for data in dataset]) 
def test_data(data):
#do test

它应该像一样运行

  • 设置(数据集A)
  • 测试(data1_a)
  • 测试(data2_a)
  • 测试(data3_a)
  • 设置(数据集B)
  • 测试(data1_b)
  • 测试(data2_b)
  • 测试(data3_b)

然而,似乎不可能像我在示例中希望的那样,对夹具获得的变量进行参数化。

我可以让我的函数使用fixture并在测试方法中迭代:

def test_data(dataset):
for data in dataset:
#do test

但我会有一个大型测试,而不是为每个案例单独测试,这是我不希望的。

有什么办法可以做到这一点吗?

谢谢!

答案#1:如果严格按照你的测试设计,那么它应该是这样的:

import pytest
datasetA = [10, 20, 30]
datasetB = [100, 200, 300]
@pytest.fixture
def dataset(request):
#do setup
items = request.param
yield items
#finalize
@pytest.fixture
def item(request, dataset):
index = request.param
yield dataset[index]
#dataset should be the same instantiated for the setup
@pytest.mark.parametrize('dataset', [datasetA, datasetB], indirect=True)
@pytest.mark.parametrize('item', [0, 1, 2], indirect=True)
def test_data(dataset, item):
print(item)
#do test

注意CCD_ 1&CCD_ 2。参数值将传递给与request.param相同的命名设备。在这种情况下,我们使用索引的假设是数据集的长度相同,共有3个项目。

以下是它的执行方式:

$ pytest -s -v -ra test_me.py 
test_me.py::test_data[0-dataset0] 10
PASSED
test_me.py::test_data[0-dataset1] 100
PASSED
test_me.py::test_data[1-dataset0] 20
PASSED
test_me.py::test_data[1-dataset1] 200
PASSED
test_me.py::test_data[2-dataset0] 30
PASSED
test_me.py::test_data[2-dataset1] 300
PASSED

Sergey的回答对我来说似乎不完整,因为它依赖于这样一个事实,即两个数据集都有相同数量的项,因此可以用等于range(3)的相同index参数进行参数化。

下面是另一个更通用的答案,允许每个数据库都有自己的项目数。它使用了pytest-cases的新版本2.0.0,与传统版本相比有了很大的改进(我在本页上给出了我的旧答案,因为它谈到了一些其他/额外的问题):

from pytest_cases import parametrize_with_cases, parametrize, fixture
datasetA = [10, 20, 30]
dbA_keys = range(3)
datasetB = [100, 200]  # just to see that it works with different sizes :)
dbB_keys = range(2)
@fixture(scope="module")
def dbA():
#do setup
yield datasetA
#finalize
@parametrize(idx=dbA_keys)
def item_from_A(dbA, idx):
yield dbA[idx]
@fixture(scope="module")
def dbB():
#do setup
yield datasetB
#finalize
@parametrize(idx=dbB_keys)
def item_from_B(dbB, idx):
yield dbB[idx]
@parametrize_with_cases('data', prefix='item_', cases='.')
def test_data(data):
print(data)
#do test

你不觉得简单多了吗?

答案#2:您还可以注入集合&pytest的参数化阶段,通过当前目录中名为conftest.py的伪插件:

conftest.py:

import pytest
datasetA = [100, 200, 300]
datasetB = [10, 20, 30]
def pytest_generate_tests(metafunc):
if 'data' in metafunc.fixturenames:
for datasetname, dataset in zip(['A', 'B'], [datasetA, datasetB]):
for data in dataset:
metafunc.addcall(dict(data=data), id=datasetname+str(data))

test_me.py:

def test_data(data):
print(data)
#do test

运行:

$ pytest -ra -v -s test_me.py 
test_me.py::test_data[A100] 100
PASSED
test_me.py::test_data[A200] 200
PASSED
test_me.py::test_data[A300] 300
PASSED
test_me.py::test_data[B10] 10
PASSED
test_me.py::test_data[B20] 20
PASSED
test_me.py::test_data[B30] 30
PASSED

然而,由于item1不支持间接参数,因此使dataset间接(即通过具有设置和拆卸阶段的夹具可访问)在这里变得困难。


添加indirect=...的唯一方法是通过metafunc.parametrize()。但在这种情况下,假设数据集大小不同,则必须构建数据集-数据项对的完整列表:

conftest.py:

import pytest
datasetA = [100, 200, 300]
datasetB = [10, 20, 30]
datasets = [datasetA, datasetB]
def pytest_generate_tests(metafunc):
if 'data' in metafunc.fixturenames:
metafunc.parametrize('dataset, data', [
(dataset, data)
for dataset in datasets
for data in dataset
], indirect=['dataset'], ids=[
'DS{}-{}'.format(idx, str(data))
for idx, dataset in enumerate(datasets)
for data in dataset
])
@pytest.fixture()
def dataset(request):
#do setup
yield request.param
#finalize

test_me.py:

def test_data(dataset, data):
print(data)
#do test

运行:

$ pytest -ra -v -s test_me.py 
test_me.py::test_data[DS0-100] 100
PASSED
test_me.py::test_data[DS0-200] 200
PASSED
test_me.py::test_data[DS0-300] 300
PASSED
test_me.py::test_data[DS1-10] 10
PASSED
test_me.py::test_data[DS1-20] 20
PASSED
test_me.py::test_data[DS1-30] 30
PASSED

EDIT:这是使用pytest用例的遗留版本的旧答案。请看这个新答案

pytest-cases提供了两种方法来解决这个问题

  • @cases_data,一个装饰器,您可以在测试函数或fixture上使用它,以便它从各种";case函数";,可能在各种模块中,并且可能自身被参数化。问题在于;case函数";不是固定装置,因此不允许您从依赖关系和设置/拆卸机制中获益。我用它来收集文件系统中的各种案例。

  • fixture_union是最近的,但更为"pytest-y",它允许您创建一个夹具,该夹具是两个或多个夹具的并集。这包括设置/拆卸和依赖关系,所以这是您在这里更喜欢的。可以显式创建并集,也可以在参数值中使用pytest_parametrize_plusdataset0来创建并集。

以下是您的示例:

import pytest
from pytest_cases import pytest_parametrize_plus, pytest_fixture_plus, fixture_ref
# ------ Dataset A
DA = ['data1_a', 'data2_a', 'data3_a']
DA_data_indices = list(range(len(DA)))
@pytest_fixture_plus(scope="module")
def datasetA():
print("setting up dataset A")
yield DA
print("tearing down dataset A")
@pytest_fixture_plus(scope="module")
@pytest.mark.parametrize('data_index', DA_data_indices, ids="idx={}".format)
def data_from_datasetA(datasetA, data_index):
return datasetA[data_index]
# ------ Dataset B
DB = ['data1_b', 'data2_b']
DB_data_indices = list(range(len(DB)))
@pytest_fixture_plus(scope="module")
def datasetB():
print("setting up dataset B")
yield DB
print("tearing down dataset B")
@pytest_fixture_plus(scope="module")
@pytest.mark.parametrize('data_index', range(len(DB)), ids="idx={}".format)
def data_from_datasetB(datasetB, data_index):
return datasetB[data_index]
# ------ Test
@pytest_parametrize_plus('data', [fixture_ref('data_from_datasetA'),
fixture_ref('data_from_datasetB')])
def test_databases(data):
# do test
print(data)

当然,您可能希望动态地处理任意数量的数据集。在这种情况下,您必须动态生成所有可替换的fixture,因为pytest必须提前知道要执行的测试数量。这很好用:

import pytest
from makefun import with_signature
from pytest_cases import pytest_parametrize_plus, pytest_fixture_plus, fixture_ref
# ------ Datasets
datasets = {
'DA': ['data1_a', 'data2_a', 'data3_a'],
'DB': ['data1_b', 'data2_b']
}
datasets_indices = {dn: range(len(dc)) for dn, dc in datasets.items()}

# ------ Datasets fixture generation
def create_dataset_fixture(dataset_name):
@pytest_fixture_plus(scope="module", name=dataset_name)
def dataset():
print("setting up dataset %s" % dataset_name)
yield datasets[dataset_name]
print("tearing down dataset %s" % dataset_name)
return dataset
def create_data_from_dataset_fixture(dataset_name):
@pytest_fixture_plus(name="data_from_%s" % dataset_name, scope="module")
@pytest.mark.parametrize('data_index', dataset_indices, ids="idx={}".format)
@with_signature("(%s, data_index)" % dataset_name)
def data_from_dataset(data_index, **kwargs):
dataset = kwargs.popitem()[1]
return dataset[data_index]
return data_from_dataset
for dataset_name, dataset_indices in datasets_indices.items():
globals()[dataset_name] = create_dataset_fixture(dataset_name)
globals()["data_from_%s" % dataset_name] = create_data_from_dataset_fixture(dataset_name)
# ------ Test
@pytest_parametrize_plus('data', [fixture_ref('data_from_%s' % n)
for n in datasets_indices.keys()])
def test_databases(data):
# do test
print(data)

两者都提供相同的输出:

setting up dataset DA
data1_a
data2_a
data3_a
tearing down dataset DA
setting up dataset DB
data1_b
data2_b
tearing down dataset DB

EDIT:如果所有数据集的设置/拆卸过程都相同,使用param_fixtures,那么可能会有一个更简单的解决方案。我会尽快把它发布出来。

编辑2:实际上,我所指的更简单的解决方案似乎会导致多次设置/拆卸,正如您在接受的答案中所指出的:

from pytest_cases import pytest_fixture_plus, param_fixtures
# ------ Datasets
datasets = {
'DA': ['data1_a', 'data2_a', 'data3_a'],
'DB': ['data1_b', 'data2_b']
}
was_setup = {
'DA': False,
'DB': False
}
data_indices = {_dataset_name: list(range(len(_dataset_contents)))
for _dataset_name, _dataset_contents in datasets.items()}
param_fixtures("dataset_name, data_index", [(_dataset_name, _data_idx) for _dataset_name in datasets
for _data_idx in data_indices[_dataset_name]],
scope='module')
@pytest_fixture_plus(scope="module")
def dataset(dataset_name):
print("setting up dataset %s" % dataset_name)
assert not was_setup[dataset_name]
was_setup[dataset_name] = True
yield datasets[dataset_name]
print("tearing down dataset %s" % dataset_name)
@pytest_fixture_plus(scope="module")
def data(dataset, data_index):
return dataset[data_index]
# ------ Test
def test_databases(data):
# do test
print(data)

我在pytest-dev上打开了一张票,以更好地理解原因:pytest-dev#5547

有关详细信息,请参阅文档。(顺便说一句,我是作者)

最新更新