Reputation: 28437
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.
Upvotes: 9
Views: 7140
Reputation: 786
Your fixture should look like this:
@pytest.fixture(scope='function')
def doc_fixture(request):
parser = request.param[0]
text = request.param[1]
return parser.parse(text)
and use it in following way:
@pytest.mark.parametrize('doc_fixture', [parser_1, 'short text'], indirect=True)
def test_sth(doc_fixture):
... # Perform tests
You can mix and match combination of arguments using pytest.mark.parametrize
Here is another example that provides different argument combinations:
from argparse import Namespace
import pytest
@pytest.fixture(scope='function')
def doc_fixture(request):
first_arg, second_arg = request.param
s = Namespace()
s.one = first_arg
s.two = second_arg
return s
@pytest.mark.parametrize(
'doc_fixture',
[
('parserA', 'ShortText'),
('parserA', 'LongText'),
('parserB', 'ShortText'),
('parserB', 'LongText')
],
indirect=True
)
def test_something(doc_fixture):
assert doc_fixture == ''
And an example run result (with failing tests as expected):
=========================================================================================== short test summary info ============================================================================================
FAILED ../../tmp/::test_something[doc_fixture0] - AssertionError: assert Namespace(one='parserA', two='ShortText') == ''
FAILED ../../tmp/::test_something[doc_fixture1] - AssertionError: assert Namespace(one='parserA', two='LongText') == ''
FAILED ../../tmp/::test_something[doc_fixture2] - AssertionError: assert Namespace(one='parserB', two='ShortText') == ''
FAILED ../../tmp/::test_something[doc_fixture3] - AssertionError: assert Namespace(one='parserB', two='LongText') == ''
Upvotes: 0
Reputation: 16815
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)
Upvotes: 1