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