4

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.

Bram Vanroy
  • 27,032
  • 24
  • 137
  • 239
  • The answer is that it isn't possible ATM, see [issue #349](https://github.com/pytest-dev/pytest/issues/349). The [`pytest-lazy-fixture`](https://pypi.org/project/pytest-lazy-fixture/) plugin claims having that functionality though, worth checking it out. – hoefling Apr 28 '20 at 22:19
  • https://github.com/pytest-dev/pytest/issues/1694 – Vivek Dani Mar 15 '23 at 14:28

2 Answers2

0

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') == ''
salparadise
  • 5,699
  • 1
  • 26
  • 32
Maciej M
  • 786
  • 6
  • 17
  • Could you explain in words in your post how the `parameterize` line should be read (how the arguments relate). – Bram Vanroy Apr 28 '20 at 13:16
  • Name of fixture, list of parameters, indirect=True specifies that params are sent to fixtrue. – Maciej M Apr 28 '20 at 13:27
  • I don't think this does what I want. If I understand correctly, you only send parser_a and short_text to the fixture, but I need to do it for all combinations of the parsers and the text. – Bram Vanroy Apr 28 '20 at 13:32
  • @BramVanroy you can mix and match combinations of `pytest.mark.parametrize` edited the answer to add a more appropriate example. – salparadise Apr 30 '20 at 05:14
  • @salparadise But now it just shows one level of fixtures, right? Aren't "'ShortText'" and "parserA" just strings? I need them to be fixtures themselves (or, as sown in MrBean's answer, by using singletons). – Bram Vanroy Apr 30 '20 at 07:38
  • @BramVanroy, sure, I mean you can pass whatever types you want (parser objects if you may) however I understand your wish to pass in fixtures. Apologies I misunderstood that part of the requirement. – salparadise Apr 30 '20 at 07:47
  • question is to pass parameter from a fixture to another fixture and not form a test method to a fixture – Vivek Dani Mar 15 '23 at 14:16
0

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)
MrBean Bremen
  • 14,916
  • 3
  • 26
  • 46
  • Thanks for your reply. Won't that mean that the parsers will be created over and over again because they are not in fixtures? – Bram Vanroy Apr 29 '20 at 12:19
  • True - if this is not wanted, this is not sufficient... I will have a look later. – MrBean Bremen Apr 29 '20 at 13:27
  • I kind of circumvented the issue by having a global dict PARSERS and in your example, `def parser_a()` becomes something like: `if 'parser_a' not in PARSERS: PARSERS['parser_a'] = Parser('parser_a'); return PARSERS['parser_a']`. This seems to work but I am not sure if it has any caveats. – Bram Vanroy Apr 29 '20 at 13:35
  • Yes, this is always a possibility - make them basically singletons. I didn't come up with another solution yet, and this will work at least. It may have side effects if the parser has a state that remains between tests - this has to be reset then. If it is stateless, there will be no problems. – MrBean Bremen Apr 29 '20 at 13:42
  • 1
    I looked a bit further, and as @hoefling has pointed out in his comment, this seems not to be possible. So using a singleton together with combined fixtures is probably your best bet. Interesting question, anyway... – MrBean Bremen Apr 29 '20 at 19:13