Mocking, Monkey Patching, and Faking Functionality¶
Sometimes while testing you need some fake data.
Maybe you’re doing a third-party API call that can be expensive in execution, or has some limit that you don’t want to risk reaching.
Or, maybe you need to create a whole object on the fly just for one or two pieces of functionality, but that object can be difficult to construct from scratch.
Or maybe your needs are simpler.
Maybe you just want to track some metrics about function/method calls in your codebase, and you have no simple way to do it outside of logging every function call with print()
statements or the Python logger
.
For these reasons and more, there exists the mock
library. Several languages have their own ways and means for mocking behavior, but mock
is a specific, pip
installable library in Python 2. It was so useful that it was built into Python 3.3+’s unittest
library.
We’re currently using pytest
, so we don’t have to worry about accessing mock
from the unittest
library; we can just use pytest-mock.
If this package is installed, then when we run py.test
we’ll have access to the mocker
fixture, which has attached all of the functionality of the unittest.mock
library.
This won’t be an exhaustive explanation of what can be done with the pytest-mock
library, but it will cover a couple of the more common use cases.
Monkey Patching¶
monkeypatch
is a part of the pytest-mock
library that allows you to intercept what a function would normally do, substituting its full execution with a return value of your own specification.
Note that monkey patching a function call does not count as actually testing that function call!
You ARE NOT actually using the function that you’ve monkey patched; you are rejecting its default behavior and subsituting it with new behavior.
Let’s take as an example the following functions for getting and parsing data from GitHub
# called users.py
import requests
import json
def get_user_followers(username):
"""Grab the JSON object from a given user's followers."""
response = requests.get('https://api.github.com/users/{}/followers'.format(username))
return response.content
def get_follower_names(username):
"""Given a username of a GitHub user, return a list of follower usernames."""
json_out = get_user_followers(username)
as_dict = json.loads(json_out)
return list(map(lambda x: x["login"], as_dict))
GitHub sets a limit on the rate at which you can access its data.
However, as we test get_follower_names
and other code that may call get_user_followers
, we’ll have to call this function over and over again.
# in test_users.py
def test_get_follower_names_returns_name_list():
from users import get_follower_names
assert 'jradavenport' in get_follower_names('nhuntwalker')
Before long, we’ll reach our rate limit (even with an API token). Any test we would run with this function after this point would automatically fail.
So what to do?
In our test file, we can “monkey patch” the call to GitHub’s API.
we can do this using the monkeypatch
fixture provided by pytest-mock
.
You don’t have to import it into the file.
All you have to do is have pip
installed pytest-mock
.
# in test_users.py
def substitute_func(username):
return '[{"login": "aishapectyo"},{"login": "jradavenport"},{"login": "kridicule"}]'
def test_get_follower_names_returns_name_list(monkeypatch):
import users
monkeypatch.setattr(users, 'get_user_followers', substitute_func)
assert 'jradavenport' in users.get_follower_names('nhuntwalker')
Notice the change in imports.
monkeypatch is an object unto itself with a variety of methods for faking attributes of other objects or whole namespaces.
In the example above, we use the .setattr
method to swap out our real users.get_user_followers
function with some other substitute function, substitute_func
.
Treating the users
module as an object, monkeypatch
changes the behavior of the get_user_followers
function inside the module when called for this test.
The substitute function in turn simply returns whatever we tell it to for the purposes of the test(s).
In the example above we hardcode a string that is a proper JSON object, just like users.get_follower_names
is expecting.
The substitute function otherwise does no work that’s not specified in the function definition.
The end result is that, FOR THIS TEST, whenever we would make the the full HTTP request to GitHub for its data we instead get back the return value of substitute_func()
.
Outside of this test, unless we use monkeypatch
again, users.get_user_followers
will work the way that it’s supposed to.
As with most testing problems, if we want to have the same behavior occur across a variety of tests, we can always set up a fixture. Remember that whenever you include a fixture in your test function, the code inside of the fixture is run in its entirety before the test itself is run. We can use that to our advantage.
# in test_users.py
import pytest
def substitute_func(username):
return '[{"login": "aishapectyo"},{"login": "jradavenport"},{"login": "kridicule"}]'
@pytest.fixture
def gh_patched(monkeypatch):
import users
monkeypatch.setattr(users, 'get_user_followers', substitute_func)
def test_get_follower_names_returns_name_list(gh_patched):
from users import get_follower_names
assert 'jradavenport' in get_follower_names('nhuntwalker')
If we want it so that across every test this behavior is patched without us having to think about it, we can set the autouse
keyword argument of pytest.fixture
to True
.
# in test_users.py
import pytest
def substitute_func(username):
return '[{"login": "aishapectyo"},{"login": "jradavenport"},{"login": "kridicule"}]'
@pytest.fixture(autouse=True)
def gh_patched(monkeypatch):
import users
monkeypatch.setattr(users, 'get_user_followers', substitute_func)
def test_get_follower_names_returns_name_list():
from users import get_follower_names
assert 'jradavenport' in get_follower_names('nhuntwalker')
Note how the test no longer includes our fixture in the parameter list.
Beware of changing behavior universally.
If we monkey patch the behavior of get_user_followers
automatically for every test, we risk not being able to actually test the function after all.
MagicMock and Faking Objects¶
Sometimes it’s not enough to patch over a single function; sometimes you need an instance of a whole object, but constructing that object is a non-trivial endeavor. Consider the following example:
def some_view(request):
if request.method == "GET":
return {}
if request.method == "POST":
new_entry = Entry(
title = request.POST['title'],
body = request.POST['body']
)
request.dbsession.add(new_entry)
return HTTPFound(request.route_url('entry_list'))
Here we have a view function that handles both GET
and POST
requests.
It’s expecting as an argument some sort of request
object, but typically we can only build request objects from a real HTTP request.
So we must either have some sort of test client set up that can send requests, or receive real requests to test our view.
(Ignore the fact that this is based on Pyramid’s pattern for building requests and that Pyramid has its own built-in DummyRequest
object)
With the MagicMock object, we can build an object that can act like a request
without having to actually be a REAL instance of any Request
class.
On that object we can define any methods or attributes that might be useful for the test.
This way, we worry less about the configuration that goes into testing a function and focus instead only on giving the function what it needs to work.
To get access to the MagicMock
object from pytest-mock
, we have to first include the mocker
fixture provided to us by pytest-mock
.
The MagicMock
object is an attribute of that fixture, and can be used as you please from there.
In our test for some_view
, we might write
def test_some_view_get_req_returns_dict(mocker):
from views import some_view
req = mocker.MagicMock()
req.method = 'GET'
assert some_view(req) == {}
In this way we test that the some_view
function given an object with a method
of GET
returns the dictionary that we expect.
Similarly, we can write more tests assuming that whatever mocked object we pass through is the real object.
def test_some_view_post_returns_redirect(mocker):
from views import some_view
req = mocker.MagicMock()
req.method = 'POST'
req.POST = {'title': 'some title', 'body': 'some body text'}
req.dbsession.add = lambda arg: None
assert isinstance(some_view(req), HTTPFound)
Here we’ve made an object with a method
attribute that has a value of 'POST'
, a POST
attribute that is a dictionary containing some values, and a dbsession
attribute that has its own fake method, add()
.
None of these are required to actually work in order to make our test pass.
We bypass the overhead of having to set up that functionality just in order to run these tests.
Instead, we make sure that our fake object has all the attributes we need to make the function work. Then we pass it to the function with the function being none the wiser.
As we can see, the MagicMock
object is pretty much sculpting clay, taking on whatever form and functionality that we need for the moment.
It’s good for unit tests of functions that don’t require that we also check for side effects.
Depending on the side effect we’re expecting, we may even be able to mock that by providing the side_effect
keyword argument on initialization of MagicMock, but if we’re getting to the point of testing interconnected functionality we may want to choose a different testing method.
mocker.spy
for Tracking Your Methods¶
Sometimes you don’t want to completely hijack a function.
Sometimes you just want to keep track of a function’s usage in your application or codebase.
For that, there’s mocker.spy
.
mocker.spy
will allow your object or function to act exactly as it normally would in all cases.
The benefit is that you can use some of the features of a MagicMock
object alongside your function or object’s regular operations.
For example, consider the following massively-inefficient object
class Numbers(object):
def __init__(self, iterable):
self._container = iterable
def make_unique(self):
i = 0
visited = []
while i < len(self._container):
if self._container[i] in visited:
self._drop_val(i)
i = 0
continue
visited.append(self._container[i])
i += 1
def _drop_val(self, idx):
self._container.pop(idx)
The method _drop_val
should be called whenever there is a value to be removed from the container list.
Perhaps for a given list of values (i.e. [1,2,1,2,1,2]
) I want to make sure that _drop_val
is called a specific number of times (i.e. 4).
I can bake that into my tests by spying on _drop_val
and checking the number of times this method was called.
# test code
def test_values_are_dropped_if_already_seen(mocker):
nums = Numbers([1,2,1,2,1,2])
mocker.spy(nums, '_drop_val')
nums.make_unique()
assert nums._make_unique.call_count == 4
And now, every time that _drop_val
was called, that tally was kept and can be verified later on.
In addition to spying on the call count, spied-on methods have a handful of other useful functionality:
assert_called_with(*args, **kwargs)
assert_any_call(*args, **kwargs)
called
mock_calls
The above is not an exhaustive list. Check the Mock Class documentation for the full details.
You can use this in a number of ways, like finding bottlenecks in your code execution by searching for methods that get called a bunch of times, troubleshooting function execution by verifying what it’s being called with, finding out what functions/methods are calling the one you’re interested in, etc.