Search code examples
pythonmodulemockingpytestdecorator

Mocking a module level function in pytest


I have a function that has a decorator. The decorator accepts arguments and the value of the argument is derived from another function call.

example.py

from cachetools import cached
from cachetools import TTLCache

from other import get_value


@cached(cache=TTLCache(maxsize=1, ttl=get_value('cache_ttl')))
def my_func():
    return 'result'

other.py

def get_value(key):
    data = {
        'cache_ttl': 10,
    }
    # Let's assume here we launch a shuttle to the space too.
    return data[key]

I'd like to mock the call to get_value(). I'm using the following in my test:

example_test.py

import mock
import pytest

from example import my_func


@pytest.fixture
def mock_get_value():
    with mock.patch(
        "example.get_value",
        autospec=True,
    ) as _mock:
        yield _mock


def test_my_func(mock_get_value):
    assert my_func() == 'result'

Here I'm injecting mock_get_value to test_my_func. However, since my decorator is called on the first import, get_value() gets called immediately. Any idea if there's a way to mock the call to get_value() before module is imported right away using pytest?


Solution

  • Move the from example import my_func inside your with in your test function. Also patch it where it's really coming from, other.get_value. That may be all it takes.


    Python caches modules in sys.modules, so module-level code (like function definitions) only runs on the first import from anywhere. If this isn't the first time, you can force a re-import using either importlib.reload() or by deleting the appropriate key in sys.modules and importing again.

    Beware that re-importing a module may have side effects, and you may also want to re-import the module again after running the test to avoid interfering with other tests. If another module was using objects defined in the re-imported module, these don't just disappear, and may not be updated the way it expects. For example, re-importing a module may create a second instance of what was supposed to be a singleton.

    One more robust approach would be save the original imported module object somewhere else, delete from sys.modules, re-import with the patched version for the duration of the test, and then put back the original import into sys.modules after the test. You could do this with an import inside of a patch.dict() context on sys.modules.

    import mock
    import sys
    
    import pytest
    
    @pytest.fixture
    def mock_get_value():
        with mock.patch(
            "other.get_value",
            autospec=True,
        ) as _mock, mock.patch.dict("sys.modules"):
            sys.modules.pop("example", None)
            yield _mock
    
    
    def test_my_func(mock_get_value):
        from example import my_func
        assert my_func() == 'result'
    

    Another possibility is to call the decorator yourself in the test, on the original function. If the decorator used functools.wraps()/functools.update_wrapper(), then the original function should be available as a __wrapped__ attribute. This may not be available depending on how the decorator was implemented.