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 fixture?
I guess it would make sense to create a new fixture that somehow concatenates the two existing fixtures. This works well for "normal" fixtures, but I don't seem to get it to work with parametrized fixtures.
Here is a simplified example of what I tried:
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):
return request.getfuncargvalue(request.param)
def test_all(all):
assert 0, all
When I run this I get this error:
request = <SubRequest 'lower' for <Function 'test_all[lower]'>>
@pytest.fixture(params=[1, 2, 3])
def lower(request):
> return "i" * request.param
E AttributeError: 'SubRequest' object has no attribute 'param'
... and the same error for upper()
.
What did I do wrong?
How can I fix this?
UPDATE:
There is a PyTest plugin that can be used to solve this problem: https://github.com/TvoroG/pytest-lazy-fixture.
After pip
-installing this plugin, the only necessary change to the above code is the following:
@pytest.fixture(params=[pytest.lazy_fixture('lower'),
pytest.lazy_fixture('upper')])
def all(request):
return request.param
Note, however, that there are some complex cases in which it will not work:
https://github.com/pytest-dev/pytest/issues/3244#issuecomment-369836702
Related PyTest issues:
Generally, the parameter can be any object, so you can always put your fixture parameters in a suitable object. With a tuple or a list parameter, you can also access the values per index as in your example.
You can pass arguments to fixtures with the params keyword argument in the fixture decorator, and you can also pass arguments to tests with the @pytest.
A fixture can use multiple other fixtures. Just like a test method can take multiple fixtures as arguments, a fixture can take multiple other fixtures as arguments and use them to create the fixture value that it returns.
usefixtures("fixture-name") . This special decorator adds the fixture to a test class, and the fixture will be executed before any test function. Check out the logs below. There is a special usage of yield statement in Pytest that allows you to execute the fixture after all the test functions.
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 ;) )
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With