pythonpython-3.xunit-testingpytestmonkeypatching

Global fixture in pytest


Question

I want, with as little boilerplate possible, to mock one of my function.

Project Setup

In my (simplified) project, I have the following files/functions:

from work import do_work

def test_work():
  # somehow have get_id patched
  ...
  do_work()
    ...

Solutions

Easy, too much boilerplate

It is very easy to patch get_id from the work module. Note that whatever the final solution is, the parameters are important, so return_value will not do.

from work import do_work
import mock

def mock_id(param1, param2): return f"{param1} {param2}"

def test_work():
  with mock.path("work.get_id", side_effect=mock_id):
    ...
    do_work()
    ...

# A variation is:

@patch("work.get_id", side_effect=mock_id)
def test_another_work(mock_id):
  ...

Does work, but it is a lot of boilerplate:

conftest.py and global fixture

I can add a conftest.py file, defining a pytest fixture once and for all

@pytest.fixture()
def _get_id():
    def mock_get_id(param1, param2):
        return f"{param1} {param2}"
    with mock.patch("utils.get_id", side_effect=mock_get_id):
        yield

I thought that then I could just have my tests written as such:

@pytest.mark.usefixtures("_get_id")
def test_work():
  ...

I explicitly do not want it with autouse=True, and this one @pytest.mark.usefixtures("_get_id") line seems to me like a good balance between boilerplate and explicitness.

alternative fixture

While looking around, this looked like it could have worked as well:

@pytest.fixture()
def _get_id(monkeypatch):
    def mock_get_id(param1, param2):
        return f"{param1} {param2}"
    monkeypath("utils.get_id", mockget_id):

Problem

The fixture is called, used, but the original get_id is always used, not the mocked version. How can I ensure that get_id is globally patched?


Solution

  • It's not possible in general in Python to replace a function with another when it has been imported with from foo import get_id into multiple places. (See "Where to patch" in unittest.mock's docs.) (You could, technically, and very trickily, maybe replace the function's internal code object with another, but that veers dangerously into black magic territory.)

    If this is about a single get_id, I would recommend to make it a trampoline:

    _test_get_id = None
    def get_id(x):
        if _test_get_id:  # For ease of testing
            return _test_get_id(x)
        ...  # usual implementation
    

    Now, you can use any method you desire to set _test_get_id (e.g. just monkeypatch.setattr), and all calls of get_id, however they may have been imported, will end up using _test_get_id.