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

1000 Views Asked by At

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

  • without changing the implementation of test_reusable_thing,
  • and without changing the implementation of fixture thing,
  • and without copy-pasting the implementation of test_reusable_thing
  • and without manually creating a wrapper function 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.

1

There are 1 best solutions below

1
On BEST ANSWER

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