pythonpytestmonkeypatchingpytest-fixtures

Defining fixtures with variable import paths


I am testing functions from different modules that use an imported helper function from helpers.py. I am monkeypatching this helper function in each module function's test (please see below). I would like to put setup_do_thing() and mock_do_thing() in a conftest.py file to cut down on repetition and remove general_mocks.py, but am unable to do this due to the different import paths supplied to monkeypatch.setattr() in test_module_1.py and test_module_2.py.

Is there a way to put the fixture in a single file and supply to multiple test modules?

app/helper.py:

def do_thing():
    #do_thing

app/module1.py:

from helper import do_thing

def use_do_thing_for_x():
    ...
    do_thing()
    ...

app/module2.py:

from helper import do_thing

def use_do_thing_for_y():
    ...
    do_thing
    ...

tests/general_mocks.py:

def mock_do_thing():
    #do_other_thing

tests/test_module_1.py:

from general_mocks import mock_do_thing

@pytest.fixture
def setup_do_thing(monkeypatch):
    monkey_patch.setattr('app.module1.do_thing', 
                         mock_do_thing)
    
def test_use_do_thing_for_x(setup_do_thing):
    ...

tests/test_module_2.py:

from general_mocks import mock_do_thing

@pytest.fixture
def setup_do_thing(monkeypatch):
    monkey_patch.setattr('app.module2.do_thing', 
                         mock_do_thing)
    
def test_use_do_thing_for_y(setup_do_thing):
    ...

Solution

  • If you want to do this, you have to parametrize your fixture in some way, as it cannot guess the correct module. First comes to mind indirect parametrization, e.g. something like:

    conftest.py

    @pytest.fixture
    def setup_do_thing(monkeypatch, request):
        # error handling omitted
        monkeypatch.setattr(f"app.{request.param}.do_thing",
                            mock_do_thing)
    

    test_module1.py

    @pytest.mark.parametrize("setup_do_thing", ["module1"], indirect=True)
    def test_use_do_thing_for_x(setup_do_thing):
        use_do_thing_for_x()
    

    If you don't like because it somewhat misuses parametrization, you can for example define your own marker instead:

    conftest.py

    def pytest_configure(config):
        # register your marker
        config.addinivalue_line(
            "markers", "do_thing_module: module where 'do_thing' resides"
        )
    
    @pytest.fixture
    def setup_do_thing(monkeypatch, request):
        mark = request.node.get_closest_marker("do_thing_module")
        if mark:
            # use the first marker argument as module name
            # error handling omitted
            monkeypatch.setattr(f"app.{mark.args[0]}.do_thing",
                                mock_do_thing)
    

    test_module1.py

    @pytest.mark.do_thing_module("module1")
    def test_use_do_thing_for_x(setup_do_thing):
        use_do_thing_for_x()
    

    There are probably more possibilities to do this, but these came to mind first.