How to apply indirect parametrization to multiple fixtures used in a single test?

Viewed 1412

I would like to indirectly parametrize multiple fixtures for a single test, very similar to what is described in the pytest documentation under "Apply indirect on particular arguments".

The test code roughly looks like this:

import pytest

@pytest.fixture
def getDataA(request):
    """Expensive method to call data A (utilizing request.param)"""
    if (request.param = "DetailedDataElementinA"):
        return True

@pytest.fixture
def getDataB(request):
    """Expensive method to call data B (utilizing request.param)"""
    if (request.param = "DetailedDataElementinB"):
        return True


@pytest.mark.parametrize("getDataA, getDataB ", ["DetailedDataElementinA", " DetailedDataElementinB"], indirect=["getDataA ", "getDataB"])
def test_comparedata(getDataA, getDataB):
    """Test that compares specific elements (detailed in request param) of two data sets"""
    assert(getDataA == getDataB)

My problem is with @pytest.mark.parametrize. The example in the pytest documentation uses only one indirect parametrization whereas I use two. I have tried several different syntax constructions but so far I was not successful and I get error messages like:

  • "E TypeError: parametrize() got multiple values for argument 'indirect'"
  • "in "parametrize" the number of names ... must be equal to the number of values"

Does anyone know which syntax may be working?

2 Answers

You are not using indirect on particular arguments, but on all arguments, so the easiest way is to use indirect=True. Apart from that, your version also should work, you just have a couple of errors in your code.

The main problem is that you provide two single values, which are interpreted as two different values to use for parametrization. You have to provide tuples of values instead, because you have two parameters. In your example, that would be only one tuple:

@pytest.mark.parametrize("getDataA, getDataB",
                         [("DetailedDataElementinA", "DetailedDataElementinB")],
                         indirect=True)
def test_comparedata(getDataA, getDataB):
    assert getDataA == getDataB

Your second problem are some random spaces you put into the strings. Spaces are part of the strings, though that may also have been an error you made on copying the code (like the assignment you use instead of comparison in the fixtures).

(it helps to paste working code into a question, otherwise it is difficult to understand what is a real error and what is just a typo)

Using indirect=['getDataA', 'getDataB'] should work, as it's supposed to pass a list or tuple of arguments’ names to indirect for fixtures to indirectly parametrize. But, from your current code:

@pytest.mark.parametrize("getDataA, getDataB ", ["DetailedDataElementinA", " DetailedDataElementinB"], indirect=["getDataA ", "getDataB"])
def test_comparedata(getDataA, getDataB):
    assert(getDataA == getDataB)

You have 2 indirectly parametrized fixtures, but you only have 1 parameter value passed to it on each test run. The 2nd argument to .parametrize should be the parameter value/s to be passed to each fixture on each test run:

If N argnames were specified, argvalues must be a list of N-tuples, where each tuple-element specifies a value for its respective argname.

The 1st run will try to pass only "DetailedDataElementinA" into 2 fixtures, and it will fail with:

test.py::test_comparedata: in "parametrize" the number of names (2):
  ['getDataA', 'getDataB']
must be equal to the number of values (22):
  DetailedDataElementinA

because it is expecting 2 values, 1 for each fixture.

Following the method's description, since there are 2 fixtures, then you need 2-tuple values, 1 value for each fixture.

(Here I simplified the sample fixtures such that the fixture simply returns the param value, and then I let the assertion fail to see the actual return values of getDataA and getDataB)

@pytest.fixture
def getDataA(request):
    return request.param.replace('ThisWillBePassedTo', 'FixtureReceived')

@pytest.fixture
def getDataB(request):
    return request.param.replace('ThisWillBePassedTo', 'FixtureReceived')

@pytest.mark.parametrize(
    'getDataA, getDataB',
    [
        ('ThisWillBePassedToA-1', 'ThisWillBePassedToB-1'),
        ('ThisWillBePassedToA-2', 'ThisWillBePassedToB-2'),
        ('ThisWillBePassedToA-3', 'ThisWillBePassedToB-3'),
    ],
    indirect=['getDataA', 'getDataB'],
)
def test_comparedata(getDataA, getDataB):
    assert (getDataA == getDataB)

There should be 3 tests (3 tuples), and on each run, tuple element at index [0] will be passed to getDataA and tuple element at index [1] will be passed to getDataB, in the same order the fixtures are defined:

test.py::test_comparedata[ThisWillBePassedToA-1-ThisWillBePassedToB-1] FAILED [ 33%]
test.py::test_comparedata[ThisWillBePassedToA-2-ThisWillBePassedToB-2] FAILED [ 66%]
test.py::test_comparedata[ThisWillBePassedToA-3-ThisWillBePassedToB-3] FAILED [100%]

getDataA = 'FixtureReceivedA-1', getDataB = 'FixtureReceivedB-1'
>       assert (getDataA == getDataB)
E       AssertionError: assert 'FixtureReceivedA-1' == 'FixtureReceivedB-1'
E         - FixtureReceivedB-1
E         ?                ^
E         + FixtureReceivedA-1
E         ?     

getDataA = 'FixtureReceivedA-2', getDataB = 'FixtureReceivedB-2'
>       assert (getDataA == getDataB)
E       AssertionError: assert 'FixtureReceivedA-2' == 'FixtureReceivedB-2'
E         - FixtureReceivedB-2
E         ?                ^
E         + FixtureReceivedA-2
E         ? 

While that worked, it can be problematic when you have many fixtures because you'll have to list up all the different combinations yourself and it can also be confusing to rely on the order.

What you can do instead is to stack the parametrized tests which will get all combinations of parameters:

@pytest.fixture
def getDataA(request):
    return request.param.replace('ThisWillBePassedTo', 'FixtureReceived')

@pytest.fixture
def getDataB(request):
    return request.param.replace('ThisWillBePassedTo', 'FixtureReceived')

@pytest.mark.parametrize(
    'getDataB',
    [
        'ThisWillBePassedToB-1',
        'ThisWillBePassedToB-2',
        'ThisWillBePassedToB-3',
    ],
    indirect=True,
)
@pytest.mark.parametrize(
    'getDataA',
    [
        'ThisWillBePassedToA-1',
        'ThisWillBePassedToA-2',
        'ThisWillBePassedToA-3',
    ],
    indirect=True,
)
def test_comparedata(getDataA, getDataB):
    assert (getDataA == getDataB)

Here, you can simply use normal indirect=True and parameter values are clearer to be for getDataA only or for getDataB only. The parametrization still follows an order: the innermost parametrized fixture should also be the innermost/first fixture passed to the function. And then it goes outward.

Running that results in:

test.py::test_comparedata[ThisWillBePassedToA-1-ThisWillBePassedToB-1] ...
test.py::test_comparedata[ThisWillBePassedToA-1-ThisWillBePassedToB-2] ...
test.py::test_comparedata[ThisWillBePassedToA-1-ThisWillBePassedToB-3] ...
test.py::test_comparedata[ThisWillBePassedToA-2-ThisWillBePassedToB-1] ...
test.py::test_comparedata[ThisWillBePassedToA-2-ThisWillBePassedToB-2] ...
test.py::test_comparedata[ThisWillBePassedToA-2-ThisWillBePassedToB-3] ...
test.py::test_comparedata[ThisWillBePassedToA-3-ThisWillBePassedToB-1] ...
test.py::test_comparedata[ThisWillBePassedToA-3-ThisWillBePassedToB-2] ...
test.py::test_comparedata[ThisWillBePassedToA-3-ThisWillBePassedToB-3] ...

getDataA = 'FixtureReceivedA-1', getDataB = 'FixtureReceivedB-3'
>       assert (getDataA == getDataB)
E       AssertionError: assert 'FixtureReceivedA-1' == 'FixtureReceivedB-3'
E         - FixtureReceivedB-3
E         ?                ^ ^
E         + FixtureReceivedA-1
E         ?      
Related