We have a big suite of reusable test cases which we run in different environments. For our suite with test case ...
@pytest.mark.system_a
@pytest.mark.system_b
# ...
@pytest.mark.system_z
test_one_thing_for_current_system():
assert stuff()
... we execute pytest -m system_a
on system A, pytest -m system_b
on system B, and so on.
We want to parametrize multiple test case for one system only, but neither want to copy-paste those test cases, nor generate the parametrization dynamically based on the command line argument pytest -m
.
Instead of copy-pasting the test case, we assign the existing function object to a variable. For ...
class TestBase:
@pytest.mark.system_a
def test_reusable_thing(self):
assert stuff()
class TestReuse:
test_to_be_altered = pytest.mark.system_z(TestBase.test_reusable_thing)
... pytest --collect-only
shows two test cases
TestBase.test_reusable_thing
TestReuse.test_to_be_altered
However, pytest.mark
on one of the cases also affects the other one. Therefore, both cases are marked as system_a
and system_z
.
Using copy.deepcopy(TestBase.test_reusable_thing)
and changing __name__
of the copy, before adding mark
does not help.
Above example is only used for illustration, as it does not actually alter the test case. For our usecase we tried something like ...
class TestBase:
@pytest.fixture
def thing(self):
return 1
@pytest.mark.system_a
# ...
@pytest.mark.system_y
def test_reusable_thing(self, thing, lots, of, other, fixtures):
# lots of code
assert stuff() == thing
copy_of_test = copy.deepcopy(TestBase.test_reusable_thing)
copy_of_test.__name__ = "test_altered"
class TestReuse:
test_altered = pytest.mark.system_z(
pytest.mark.parametrize("thing", [1, 2, 3])(copy_of_test)
)
Because of aforementioned problem, this parametrizes test_reusable_thing
for all systems while we only wanted to parametrize the copy for system_z
.
How can we parametrize test_reusable_thing
for system_z
...
test_reusable_thing
,thing
,test_reusable_thing
def test_altered
for which we have to copy-paste requested fixtures only to pass them to TestBase().test_reusable_thing(thing, lots, of, other, fixtures)
.Somehow pytest has to link the copy to the original. If we know how (e.g. based on a variable like __name__
) we could break the link.
You can defer the parametrization to the pytest_generate_tests
hookimpl. You can use that to add your custom logic for implicit populating of test parameters, e.g.
def pytest_generate_tests(metafunc):
# test has `my_arg` in parameters
if 'my_arg' in metafunc.fixturenames:
marker_for_system_z = metafunc.definition.get_closest_marker('system_z')
# test was marked with `@pytest.mark.system_z`
if marker_for_system_z is not None:
values_for_system_z = some_data.get('z')
metafunc.parametrize('my_arg', values_for_system_z)
A demo example to pass the marker name to test_reusable_thing
via a system
arg:
import pytest
def pytest_generate_tests(metafunc):
if 'system' in metafunc.fixturenames:
args = [marker.name for marker in metafunc.definition.iter_markers()]
metafunc.parametrize('system', args)
class Tests:
@pytest.fixture
def thing(self):
return 1
@pytest.mark.system_a
@pytest.mark.system_b
@pytest.mark.system_c
@pytest.mark.system_z
def test_reusable_thing(self, thing, system):
assert system.startswith('system_')
Running this will yield four tests in total:
test_spam.py::Tests::test_reusable_thing[system_z] PASSED
test_spam.py::Tests::test_reusable_thing[system_c] PASSED
test_spam.py::Tests::test_reusable_thing[system_b] PASSED
test_spam.py::Tests::test_reusable_thing[system_a] PASSED