'How to create a parametrized fixture that is dependent on value of another parametrized fixture?

Example:

from pytest import fixture, skip

@fixture(params=['a', 'b'])
def f1(request):
    yield request.param


params = [('a', 1), ('a', 2), ('b', 10), ('b', 20)]
@fixture(params=params, ids=[str(x) for x in params])
def f2(f1, request):
    if request.param[0] == f1:
        yield request.param[1]
    else:
        skip('invalid')

def test_foo(f1, f2):
    return

That is a "base" fixture f1. And then "stacked" fixture f2 which should produce values (1, 2) for f1='a' and values (10, 20) for f1='b'

gives:

a.py::test_foo[a-('a', 1)] PASSED
a.py::test_foo[a-('a', 2)] PASSED
a.py::test_foo[a-('b', 10)] SKIPPED
a.py::test_foo[a-('b', 20)] SKIPPED
a.py::test_foo[b-('a', 1)] SKIPPED
a.py::test_foo[b-('a', 2)] SKIPPED
a.py::test_foo[b-('b', 10)] PASSED
a.py::test_foo[b-('b', 20)] PASSED

which is close to what I want to achieve, except I'd like to improve this so

  • the SKIPPED variants do not show up as a testcase at all
  • the parametrized testcase id shows up without repetition of the f1 fixture. Ideally like <f1-value><separator><f2-value> like a-2, b-10, etc.
  • ideally defined in some saner way, without repetition

alternative 1: parametrize workaround

There is kind of a workaround using parametrize:

@pytest.mark.parametrize(params=('f1,f2', (('a', 1), ('b', 10))))
def test_foo():
    return

However this is not really a solution as it works only for relatively simple and isolated setups. Where it breaks down is when the base fixture f1 is used in a lot of test cases and there are additional fixtures f1_n which stack on top of f1 only like:

@fixture
def f1_1(f1):
    return

At this point, using the parametrize workaround causes duplication and hurts re-usability of other stacked fixtures.

alternative 2: pytest_generate_tests

https://docs.pytest.org/en/latest/parametrize.html

f2_vals = {'a': (1,2), 'b': (10,20)}

def pytest_generate_tests(metafunc):
    if "f2" in metafunc.fixturenames:
        assert "f1" in metafunc.fixturenames
        metafunc.parametrize("f2", f2_vals[get_f1_val()])

But I don't see how to implement the get_f1_val and have little faith in this approach as it seems pytest's runs in phases where it first collects testcases and then executes them.

alternative 3: test case ignore / deslect

At the moment a feature request at https://github.com/pytest-dev/pytest/issues/3730 but solves only the part of removing the invalid testcase combinations from test session results.



Solution 1:[1]

Further effort for your alternative 2:

f2_vals = {'a': (1,2), 'b': (10,20)}

params = [(key, value) for key in f2_vals for value in f2_vals[key]]


@pytest.mark.parametrize("f1, f2", params)
def test_foo(f1, f2):
    return

Solution 2:[2]

I have a solution, it's not perfect but it works. The key to it is the getfixturevalue method of the request object (https://docs.pytest.org/en/6.2.x/reference.html#pytest.FixtureRequest.getfixturevalue)

from pytest import fixture

@fixture()
def f1_a(request):
    return "a"

@fixture()
def f1_b(request):
    return "b"


@fixture(params=["a", "b"])
def f1(request):
    return request.getfixturevalue(f"f1_{request.param}")


params = [('a', 1), ('a', 2), ('b', 10), ('b', 20)]


@fixture(params=params, ids=[str(x) for x in params])
def f2(request):
    return request.getfixturevalue(f"f1_{request.param[0]}"), request.param
    
def test_f2(f2):
    assert f2

def test_f1(f1):
    assert f1

It produces the following tests:

================================================= test session starts =================================================
platform win32 -- Python 3.8.3, pytest-7.1.1, pluggy-1.0.0 -- c:\users\miki\oss\stack\venv\scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\miki\oss\stack
collected 6 items

test_stack.py::test_f2[('a', 1)] PASSED                                                                          [ 16%]
test_stack.py::test_f2[('a', 2)] PASSED                                                                          [ 33%]
test_stack.py::test_f2[('b', 10)] PASSED                                                                         [ 50%]
test_stack.py::test_f2[('b', 20)] PASSED                                                                         [ 66%]
test_stack.py::test_f1[a] PASSED                                                                                 [ 83%]
test_stack.py::test_f1[b] PASSED                                                                                 [100%]

================================================== 6 passed in 0.06s ==================================================

The annoying bit of course is that you need to have a fixture for each of the params for f1. Practically then of course matters on how many params you have, but it might be worth it (with a function to actually do the setup within f1_a and f1_b).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Hasan Salim Kanmaz
Solution 2 mikicz