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.
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)