Search code examples
pythonpytestfixturesparameterized-unit-test

How to pass a parameterised fixture as a parameter to another fixture


I am trying to avoid repeating too much boilerplate in my tests, and I want to rewrite them in a more structured way. Let's say that I have two different parsers that both can parse a text into a doc. That doc would then be used in other tests. The end goal is to expose a doc() fixture that can be used in other tests, and that is parameterised in such a way that it runs all combinations of given parsers and texts.

@pytest.fixture
def parser_a():
    return "parser_a"  # actually a parser object

@pytest.fixture
def parser_b():
    return "parser_b"  # actually a parser object

@pytest.fixture
def short_text():
    return "Lorem ipsum"

@pytest.fixture
def long_text():
    return "If I only knew how to bake cookies I could make everyone happy."

The question is, now, how to create a doc() fixture that would look like this:

@pytest.fixture(params=???)
def doc(parser, text):
    return parser.parse(text)

where parser is parameterised to be parser_a and parser_b, and text to be short_text and long_text. This means that in total doc would test four combinations of parsers and text in total.

The documentation on PyTest's parameterised fixtures is quite vague and I could not find an answer on how to approach this. All help welcome.


Solution

  • Not sure if this is exactly what you need, but you could just use functions instead of fixtures, and combine these in fixtures:

    import pytest
    
    class Parser:  # dummy parser for testing
        def __init__(self, name):
            self.name = name
    
        def parse(self, text):
            return f'{self.name}({text})'
    
    
    class ParserFactory:  # do not recreate existing parsers
        parsers = {}
    
        @classmethod
        def instance(cls, name):
            if name not in cls.parsers:
                cls.parsers[name] = Parser(name)
            return cls.parsers[name]
    
    def parser_a():
        return ParserFactory.instance("parser_a") 
    
    def parser_b():
        return ParserFactory.instance("parser_b")
    
    def short_text():
        return "Lorem ipsum"
    
    def long_text():
        return "If I only knew how to bake cookies I could make everyone happy."
    
    
    @pytest.fixture(params=[long_text, short_text])
    def text(request):
        yield request.param
    
    @pytest.fixture(params=[parser_a, parser_b])
    def parser(request):
        yield request.param
    
    @pytest.fixture
    def doc(parser, text):
        yield parser().parse(text())
    
    def test_doc(doc):
        print(doc)
    

    The resulting pytest output is:

    ============================= test session starts =============================
    ...
    collecting ... collected 4 items
    
    test_combine_fixt.py::test_doc[parser_a-long_text] PASSED                [ 25%]parser_a(If I only knew how to bake cookies I could make everyone happy.)
    
    test_combine_fixt.py::test_doc[parser_a-short_text] PASSED               [ 50%]parser_a(Lorem ipsum)
    
    test_combine_fixt.py::test_doc[parser_b-long_text] PASSED                [ 75%]parser_b(If I only knew how to bake cookies I could make everyone happy.)
    
    test_combine_fixt.py::test_doc[parser_b-short_text] PASSED               [100%]parser_b(Lorem ipsum)
    
    
    ============================== 4 passed in 0.05s ==============================
    

    UPDATE: I added a singleton factory for the parser as discussed in the comments as an example.

    NOTE: I tried to use pytest.lazy_fixture as suggested by @hoefling. That works, and makes it possible to pass the parser and text directly from a fixture, but I couldn't get it (yet) to work in a way that each parser is instantiated only once. For reference, here is the changed code if using pytest.lazy_fixture:

    @pytest.fixture
    def parser_a():
        return Parser("parser_a")
    
    @pytest.fixture
    def parser_b():
        return Parser("parser_b")
    
    @pytest.fixture
    def short_text():
        return "Lorem ipsum"
    
    @pytest.fixture
    def long_text():
        return "If I only knew how to bake cookies I could make everyone happy."
    
    
    @pytest.fixture(params=[pytest.lazy_fixture('long_text'),
                            pytest.lazy_fixture('short_text')])
    def text(request):
        yield request.param
    
    @pytest.fixture(params=[pytest.lazy_fixture('parser_a'),
                            pytest.lazy_fixture('parser_b')])
    def parser(request):
        yield request.param
    
    
    @pytest.fixture
    def doc(parser, text):
        yield parser.parse(text)
    
    
    def test_doc(doc):
        print(doc)