Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
like image 541
piRSquared Avatar asked Jun 13 '18 17:06

piRSquared


4 Answers

Just came across this answer because I was trying to do something similar. You can use decorators like this to do preprocessing of the tests. Other pytest decorators can be added below the mock_func_1_in_test decorator.

# contents of "test_mymodule.py"

import mymodule
import pytest

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

    return mock_ret

def mock_func_1_in_test(func):
    def inner(monkeypatch, mock_func1, *args, **kwargs):
        monkeypatch.setattr(mymodule, "func1", mock_func1)
        return func(*args, **kwargs)
    return inner

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

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

This works as you'd expect:

$ pytest
================================== test session starts ===================================
platform darwin -- Python 3.6.6, pytest-3.6.0, py-1.6.0, pluggy-0.6.0
rootdir: /Users/delgadom/git/messin/pytest_test, inifile:
plugins: cov-2.5.1
collected 2 items

test_mymodule.py ..                                                                [100%]

================================ 2 passed in 0.03 seconds ================================
like image 151
Michael Delgado Avatar answered Oct 11 '22 05:10

Michael Delgado


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.

like image 41
Avezan Avatar answered Oct 11 '22 06:10

Avezan


This is what I do to keep things slightly clean. Just define a pytest fixture to patch the module in conftest.py. But still, this fixture needs to be passed to each of our tests.

contents of conftest.py

import mymodule
import pytest


@pytest.fixture(autouse=True)
def patch_mymodule(monkeypatch):
    def mock_func1(*args, **kwargs):
        return 2
    monkeypatch.setattr(mymodule, "func1", mock_func1)

contents of "mymodule.py"

def func1(x):
    return x * 2

contents of "test_mymodule.py"

import mymodule
import pytest


def test_func1_a(patch_mymodule):
    # you can pass anything to the function below, 1 is just some arbitrary choice
    assert mymodule.func1(1) == 2

def test_func1_b(patch_mymodule):
    assert mymodule.func1(1) != 37 
like image 22
bad programmer Avatar answered Oct 11 '22 04:10

bad programmer


Just add autouse=True as an argument to the fixture decorator.

@pytest.fixture(autouse=True)
def mock_func1():
    def mock_ret(*args, **kwargs):
        return 2
like image 21
Cain Avatar answered Oct 11 '22 06:10

Cain