参数化测试也取决于 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
它应该运行像:
- 设置(数据集A)
- 测试(data1_a)
- 测试(data2_a)
- 测试(data3_a)
- 设置(数据集B)
- 测试(data1_b)
- 测试(data2_b)
- 测试(data3_b)
然而,似乎无法像我在示例中希望的那样对夹具获得的变量进行参数化。
我可以让我的函数使用夹具并在测试方法中迭代:
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
请注意 item
和 dataset
的间接参数化。参数值将传递给与 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_plus
和 fixture_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
你不觉得更简单吗?
我有一个测试,我有一个设置方法,它应该接收一个 数据集 ,以及一个测试函数,每个 应该 运行 数据集
中的]数据基本上我需要这样的东西:
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)
然而,似乎无法像我在示例中希望的那样对夹具获得的变量进行参数化。
我可以让我的函数使用夹具并在测试方法中迭代:
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
请注意 item
和 dataset
的间接参数化。参数值将传递给与 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_plus
和fixture_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
你不觉得更简单吗?