Python: How you import impacts how you mock

Recently, I had a problem with monkeypatching external service based function. In a nutshell, monkeypatch is a pytest fixture that allows you to replace an object or function with your mock version. Try as I might I could not get it to work. This blog post is something I wish I had read when getting my mocks to work first time.

This is the module structure with get_user function that connects to LDAP. We want to mock get_user in our tests to avoid connecting to LDAP and to not have to use real users for tests.

base.py

def get_user(uun):
    return ca_user

SCENARIO 1: importing get_user function in the module where it’s required

forms.py

from central_auth.base import get_user

def clean_user(uun):
   ca_user = get_user(uun)

Trying to monkeypatch it as below will not have the desired effect. The real get_user still gets called.

tests.py

from central_auth import base

def test_form_POST_OK(monkeypatch):
    monkeypatch(base, 'get_user', get_mock_user_function)

    get_view_with_form() # where get_user is called

# DOES NOT WORK

This is because when we do named importation i.e. importing object or function as opposed to module, the object/function gets a new namespaced name. This way what exists as central_auth.base.get_user is referred to forms.get_user within forms.py.

To make the monkeypatch work, base module should be imported like so:

SCENARIO 2: module import

forms.py

from central_auth import base

def clean_user(uun):
   ca_user = base.get_user(uun)

Then we end up having central_auth.base.get_user and forms.base.get_user, both referring to the same base module.

Alternatively, we can use unittest.mock.patch to the same effect which also allows a greater level of granularity:

SCENARIO 1:

forms.py

from central_auth.base import get_user

def clean_user(uun):
   ca_user = get_user(uun)
tests.py

@mock.patch('forms.get_user', side_effect=get_mock_user_function)
def test_form_OK(form_get_user)

SCENARIO 2:

forms.py

from central_auth import base

def clean_user(uun):
   ca_user = base.get_user(uun)
tests.py

@mock.patch('forms.base.get_user', side_effect=get_mock_user_function)
def test_form_OK(forms_get_user)

The only disadvantage of using mock.patch is that if get_user is called in different modules in your tested function then you need to mock all of them specifically like so:

forms.py

from central_auth import base

def clean_user(uun):
   ca_user = base.get_user(uun)
views.py

from central_auth import base

def view_user(request, uun):
    ca_user = base.get_user(uun)
tests.py

@mock.patch('forms.base.get_user', side_effect=get_mock_user_function)
@mock.patch('views.base.get_user', side_effect=get_mock_user_function)
def test_form_OK(views_get_user, forms_get_user):
    get_view_with_form()

 

Since I am a newbie to python and pytest, give me a shout if I got something terribly wrong. For now, my tests work 🙂

Cheers