Search code examples
pythonpytestmonkeypatching

How to monkey patch a function for multiple tests


Consider my module "mymodule.py"

# contents of "mymodule.py"

def func1(x):
    return x * 2

I want to mock this function and alter its return. Per the documentation I can do this:

# contents of "test_mymodule.py"

import mymodule
import pytest

@pytest.fixture
def mock_func1():
    def mock_ret(*args, **kwargs):
        return 2

def test_func1_a(monkeypatch, mock_func1):
    monkeypatch.setattr(mymodule, "func1", mock_func1)
    assert mymodule.func1(1) == 2 

def test_func1_b(monkeypatch, mock_func1):
    monkeypatch.setattr(mymodule, "func1", mock_func1)
    assert mymodule.func1(1) != 37 

However, I don't want to monkey patch the module for each test. What is the proper way to monkeypatch.setattr once for the scope of the whole test module test_mymodule.py?

I'd expect something like this

# contents of "test_mymodule.py"

import mymodule
import pytest

@pytest.fixture
def mock_func1():
    def mock_ret(*args, **kwargs):
        return 2

monkeypatch.setattr(mymodule, "func1", mock_func1)

def test_func1_a():
    assert mymodule.func1(1) == 2 

def test_func1_b():
    assert mymodule.func1(1) != 37 

But this gets me

NameError: name 'monkeypatch' is not defined

Solution

  • Stolen directly from pytest:

    import mymodule
    import pytest
    
    def wildpatch(target, name, value=None, raising=True):
        import inspect
    
        if value is None:
            if not isinstance(target, _basestring):
                raise TypeError("use setattr(target, name, value) or "
                                "setattr(target, value) with target being a dotted "
                                "import string")
            value = name
            name, target = derive_importpath(target, raising)
    
        oldval = getattr(target, name, None)
        if raising and oldval is None:
            raise AttributeError("%r has no attribute %r" % (target, name))
    
        # avoid class descriptors like staticmethod/classmethod
        if inspect.isclass(target):
            oldval = target.__dict__.get(name, None)
        setattr(target, name, value)
    
    
    ##@pytest.fixture
    ##def mock_func1():
    ##    def mock_ret(*args, **kwargs):
    ##        print("monkeypatched func1")
    ##        return 2
    
    def mock_func1(*args, **kwargs):
        print("monkeypatched func1")
        return 2 
    
    wildpatch(mymodule, "func1", mock_func1)
    
    def test_func1_a():
        print("Running test_func1_a")
        assert mymodule.func1(1) == 2 
    
    def test_func1_b():
        assert mymodule.func1(1) != 37
    

    On running with python -m pytest -s test.py yields

    =============================== test session starts ================
    platform linux -- Python 3.4.3, pytest-3.1.2, py-1.4.34, pluggy-0.4.0
    rootdir: /tmp/ab, inifile:
    collected 2 items 
    
    test.py Running test_func1_a
    monkeypatched func1
    .monkeypatched func1
    .
    
    =========================== 2 passed in 0.01 seconds ===============================
    

    I have guessed all you want is too redirect func1 to your own function.