Parametrize session fixture based on config option

Question:

I’m using pytest and have something like the following in conftest.py:

def pytest_addoption(parser):
    parser.addoption('--foo', required=True, help='Foo name.')


@pytest.fixture(scope='session')
def foo(pytestconfig):
    with Foo(pytestconfig.getoption('foo')) as foo_obj:
        yield foo_obj

I’d like to change the --foo option to

    parser.addoption('--foo', action='append', help='Foo names.')

and have a separate Foo object with session scope generated for each provided name. Ordinarily, I’d use pytest_generate_tests to parametrize a fixture in this way. That is,

def pytest_generate_tests(metafunc):
    if 'foo' in metafunc.fixturenames:
        metafunc.parametrize('foo', map(Foo, metafunc.config.getoption('foo')))

However, If I’m understanding correctly how pytest_generate_tests works, Foo objects will be created separately for each test function thus defeating the whole point of a session fixture.

Asked By: Daniel Walker

||

Answers:

Seems like the easiest way to solve this is to make the fixture indirect. That is, you pass the configuration values to the fixture, but then let it manage its own setup. That way pytest will honour the fixture’s scope setting. For example:

def pytest_generate_tests(metafunc):
    # assuming --foo has been set as a possible parameter for pytest
    if "foo" in metafunc.fixturenames and metafunc.config.option.foo is not None:       
        metafunc.parametrize("foo", metafunc.config.option.foo, indirect=True)

@pytest.fixture(scope='session')
def foo(request):
    if not hasattr(request, 'param'):
        pytest.skip('no --foo option set')
    elif isinstance(request.param, str):
        return Foo(request.param)
    else:
        raise ValueError("invalid internal test config")

Altogether this looks like:

conftest.py

def pytest_addoption(parser):
    parser.addoption('--foo', action='append', help='Foo value')

test_something.py

import pytest

# keep track of how many Foos have been instantiated
FOO_COUNTER = 0

class Foo:
    def __init__(self, value):
        global FOO_COUNTER
        self.value = value
        self.count = FOO_COUNTER
        FOO_COUNTER += 1
        print(f'creating {self} number {self.count}')

    def __repr__(self):
        return f'Foo({self.value})'

def pytest_generate_tests(metafunc):
    if "foo" in metafunc.fixturenames and metafunc.config.option.foo is not None:       
        metafunc.parametrize("foo", metafunc.config.option.foo, indirect=True)

@pytest.fixture(scope='session')
def foo(request):
    if not hasattr(request, 'param'):
        pytest.skip('no --foo option set')
    elif isinstance(request.param, str):
        return Foo(request.param)
    else:
        raise ValueError("invalid internal test config")

def test_bar(foo):
    assert isinstance(foo, Foo)
    assert foo.value in list('abx')

def test_baz(foo):
    assert isinstance(foo, Foo)
    assert foo.value in list('aby')

# test name is to encourage pytest to run this test last (because of side 
# effects of Foo class has on FOO_COUNTER)
def test_zzz_last():
    # only passes when exactly two foo options set
    assert FOO_COUNTER == 2

If run with exactly two --foo options then the test_zzz_last passes. One or three --foo options and this test fails. This demonstrates that exactly one instance of Foo is created per --foo option, and each instance is shared between tests. Zero --foo options will cause any test requiring the foo fixture to be skipped.

If a --foo option is given a value that is not a, b, x or y then then both test_bar and test_baz will fail. Thus we can see that our configuration options are making it into the foo fixture.

Answered By: Dunes
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.