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

Parametrizing tests depending of also parametrized values in 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

它应该运行像:

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

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

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

请注意 itemdataset 的间接参数化。参数值将传递给与 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

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

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

然而,使 dataset 间接(即通过安装和拆卸阶段的夹具访问)在这里变得困难,因为 metafunc.addcall() 不支持间接参数。


添加 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

编辑:这是一个使用旧版 pytest-cases 的旧答案。请看

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

  • @cases_data,一个装饰器,你可以在你的测试函数或 fixture 上使用,这样它就可以从各种“案例函数”中获取参数,可能在不同的模块中,也可能是它们自己参数化.问题是“案例功能”不是固定装置,因此不允许您从依赖性和 setup/teardown 机制中受益。我用它来收集文件系统中的各种案例。

  • 更新但更多 'pytest-y'、fixture_union 允许您创建一个由两个或多个灯具组合而成的灯具。这包括 setup/teardown 和依赖项,所以这是您在这里更喜欢的。您可以显式创建联合,也可以在参数值中使用 pytest_parametrize_plusfixture_ref()

您的示例如下所示:

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)

当然,您可能希望动态处理任意数量的数据集。在那种情况下,您必须动态生成所有替代装置,因为 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

编辑:如果 setup/teardown 过程对于所有数据集都是相同的,使用 param_fixtures,可能会有一个更简单的解决方案。我会尽快 post。

编辑 2:实际上我所指的更简单的解决方案似乎会导致多个 setup/teardown 正如您在接受的答案中已经指出的那样:

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#5457

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

的答案对我来说似乎不完整,因为它依赖于两个数据集具有相同数量的项目,因此可以使用等于 [=] 的相同 index 参数进行参数化12=].

这是另一个更通用的答案,允许每个数据库拥有自己的项目数。它使用 pytest-cases 的新版本 2.0.0,它比旧版本有了很大改进(我在这个页面上留下了我的旧答案,因为它谈到了一些 other/additional 问题):

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

你不觉得更简单吗?