pythonpython-3.xpytestparametrized-testing

pytest.mark.parametrize a copy of a test case without altering the original


Background

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.

Goal

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.

Our Attempt

Copy And Mark

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.

Copy And Parametrize

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.

Question

How can we parametrize test_reusable_thing for system_z ...

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.


Solution

  • 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