Pytest and the Meta Fixture

Meta fixtures, or fixtures that control the use/parametrization of other fixtures, are mentioned in several places (eg; this issue, this proposal, this SO question of mine), but pytest (as far as I can tell) currently doesn't have a clean, obvious way of doing this that works for all cases.

Let me explain my use-case: I have several fixtures that all return similar yet different objects. There are some tests that should be run only on a specific fixture's output, and others that should be run on the outputs of all the fixtures. A meta-fixture to parametrize the outputs of the other fixtures seems like an elegant solution. Here is a completely contrived example: Let's assume we need to write some unit tests for python strings. We might have the following fixtures:

In [ ]:
import pytest


@pytest.fixture
def str_from_int():
    """ return a string from an int """
    return str(42)


@pytest.fixture
def str_from_letters():
    """ return a str from letters only """
    return 'abc'


@pytest.fixture
def empty_str():
    """ return an empty str """
    return ''


@pytest.fixture
def none_str():
    """ return str rep of None """
    return str(None)

Now we might have some tests that only should run on strings from ints, some that should only run on strings from letters, and so on.

In [ ]:
class TestStrInts:
    """ tests for str returned from ints """
    def test_is_numeric(self, str_from_int):
        """ ensure a str rep of an in is numeric """
        assert str_from_int.isnumeric()


class TestStrLetters:
    """ tests for strs returned from pure letters """
    def test_is_not_numeric(self, str_from_letters):
        """ should not be numeric """
        assert not str_from_letters.isnumeric()
        
    def test_is_alphanumeric(self, str_from_letters):
        """ should be alpha numeric """
        assert str_from_letters.isalnum()


class TestEmptyStr:
    """ tests for empty strs """
    def test_bool_is_false(self, empty_str):
        """ tests that bool rep of empty str is false """
        assert not empty_str

But there are also some tests that should run on all string fixtures. The cleanest approach for this would be to combine all these fixtures into a single parametrized fixture, let's call it "general_str".

In [ ]:
class TestStrGeneral:
    """ general tests that should pass for ALL str fixtures """
    expected_methods = ('strip', 'center', 'format', 'index', 'replace',
                        'title')  # etc.

    def test_has_expected_methods(self, general_str):
        """ ensure strs have expected methods """
        for method_name in self.expected_methods:
            assert hasattr(general_str, method_name)

Simply defining the "general_str" fixture in this way, however, wont work:

In [ ]:
@pytest.fixture(params=(str_from_int, str_from_letters, empty_str, none_str))
def general_str(request):
    """ meta fixture for collect all fixtures that return strs """
    return request.param

because request.param returns a reference to the fixture function itself, not its output. A fairly straight-forward work-around, based on this comment is pass a sequence of fixture names as strings to params, then to use the getfuncargvalue, or the more modern getfixturevalue, attribute of the request object:

In [ ]:
str_fixtures = ('str_from_int', 'str_from_letters', 'empty_str', 'none_str')

@pytest.fixture(params=str_fixtures)
def general_str(request):
    """ meta fixture for collect all fixtures that return strs """
    return request.getfixturevalue(request.param)

Now our tests will run as intended.

However, there are some limitations. Mainly, if one of the base fixtures is itself a parametrized fixture:

In [ ]:
@pytest.fixture(params=range(100))
def str_from_int(request):
    """ return a string from an int """
    return str(request.param)

We will get an error with the message: :Failed: The requested fixture has no parameter defined for the current test."

At first glance I thought pytest-lazy-fixture would solve this problem, so I installed it and rewrote the fixtures like so:

In [ ]:
# --- helper functions


def lazify(*args):
    """ return a list of lazy fixtures from args """
    return [pytest.lazy_fixture(x) for x in args]


# --- Module level fixtures


@pytest.fixture(params=range(100))
def str_from_int(request):
    """ return a string from an int """
    return str(request.param)


@pytest.fixture
def str_from_letters():
    """ return a str from letters only """
    return 'abc'


@pytest.fixture
def empty_str():
    """ return an empty str """
    return ''


@pytest.fixture
def none_str():
    """ return str rep of None """
    return str(None)


str_fixtures = ('str_from_int', 'str_from_letters', 'empty_str', 'none_str')

@pytest.fixture(params=lazify(str_fixtures))
def general_str(request):
    """ meta fixture for collect all fixtures that return strs """
    return request.getfixturevalue(request.param)

However, I get the same error message, so maybe it doesn't solve this issue? I will dig into it more when I get a chance.