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