If I have two parametrized fixtures, how can I create a single test function that is called first with the instances of one fixture and then with the instances of the other
I had the exact same question (and received a similar, but distinct answer). The best solution I was able to come up with was to reconsider how I parametrize my tests. Instead of having multiple fixtures with compatible outputs, I used the fixtures as regular functions, and just parametrized your meta-fixture to accept the function name and arguments:
import pytest
def lower(n):
return 'i' * n
def upper(n):
return 'I' * n
@pytest.fixture(params=[
(lower, 1),
(lower, 2),
(upper, 1),
(upper, 2),
(upper, 3),
])
def all(request):
func, *n = request.param
return func(*n)
def test_all(all):
...
In your particular case, unpacking n
into a list and passing it with *
is slightly overkill, but it provides generality. My case has fixtures that all accept different parameter lists.
Until pytest allows us to properly chain fixtures, this is the only way I have come up with to run 5 tests instead of 12 in your situation. You can make the list shorter with something like
@pytest.fixture(params=[
*[(lower, i) for i in range(1, 3)],
*[(upper, i) for i in range(1, 4)],
])
There is an actual advantage of doing it this way. You can pick and chose which tests you want to do special things to, like XFAIL, without affecting a whole swath of other tests if you have additional dependencies in your pipeline.
There is now a solution available in pytest-cases
, named fixture_union
. Here is how you create the fixture union that you requested in your example:
from pytest_cases import fixture_union, pytest_fixture_plus
@pytest_fixture_plus(params=[1, 2, 3])
def lower(request):
return "i" * request.param
@pytest_fixture_plus(params=[1, 2])
def upper(request):
return "I" * request.param
fixture_union('all', ['lower', 'upper'])
def test_all(all):
print(all)
It works as expected:
<...>::test_all[lower-1]
<...>::test_all[lower-2]
<...>::test_all[lower-3]
<...>::test_all[upper-1]
<...>::test_all[upper-2]
Note that I used pytest_fixture_plus
in the above example because if you use pytest.fixture
you will have to handle yourself the cases where a fixture is not actually used. This is done as follows, for example for the upper
fixture:
import pytest
from pytest_cases import NOT_USED
@pytest.fixture(params=[1, 2])
def upper(request):
# this fixture does not use pytest_fixture_plus
# so we have to explicitly discard the 'NOT_USED' cases
if request.param is not NOT_USED:
return "I" * request.param
See documentation for details. (I'm the author by the way ;) )
It is not beautiful, but may be today you know the better way.
Request object inside 'all' fixture know only about own params: 'lower', 'upper'. One way using fixtures from a fixture function.
import pytest
@pytest.fixture(params=[1, 2, 3])
def lower(request):
return "i" * request.param
@pytest.fixture(params=[1, 2])
def upper(request):
return "I" * request.param
@pytest.fixture(params=['lower', 'upper'])
def all(request, lower, upper):
if request.param == 'lower':
return lower
else:
return upper
def test_all(all):
assert 0, all