Mocking for Unit Testing
Overview
Teaching: 25 min
Exercises: 20 minQuestions
How can we test components in isolation that depend on an external environment or other components?
Objectives
Describe how to use unittest.mock to replace either a remote call or a dependent function
Use mocking to add to the test suite for our inflammation example
Mocking is a testing term that means to replace a real object with a pretend object. One of the most common objections to unit testing is that it can be quite difficult to test each function or class method in isolation, because of the dependencies that exist in all but the simplest code. For example:
- Your function/class might depend on a remote resource such as a website, FTP server or a cloud-based API that you are calling
- Function A, that you wish to tests, might use another function B as part of its work
- The class you wish to test, class A, might contain an instance of another class B that is difficult to construct (i.e. perhaps it depends on loading a large and cumbersome dataset).
For example 1, you would like your tests to pass even if the computer in question has no internet. For example 2, ideally you would want to be able to write a test for function A that didn’t depend on function B, so that if a bug occurred in function B, your tests for function A would still pass. For example 3, it would be nice to be able to test class A without having to construct the difficult-to-create object of class B.
For all these reasons, and many others, you can use mocking libraries to make your unit tests easier to write. There are many such libraries/frameworks that exist for different languages, for example:
- C: CMocka
- C++: googletest
- Python: unittest.mock
Recording calls with mock
This lesson demonstrates the use of unittest.mock
. This is a mocking framework within
the Python3 standard library, and is therefore quite convenient to use. The
documentation for unittest.mock
is
here. This framework contains a
class Mock
, which is callable and records all the calls that are made to it. For
example, below we create an object of class Mock
, and tell it to return 2
on every
function call.
from unittest.mock import Mock
function = Mock(name="myroutine", return_value=2)
function(1)
2
We can call our fake “function” with any number of arguments, with any type. However we
call the function, it will always return the value 2
.
function(5, "hello", a=True)
2
We can interrogate the Mock
object to allow us to see how it has been called, we can
use this functionality in tests to assert that the mock object is being called
correctly. The mock_calls
attribute returns an array of calls made to function
.
function.mock_calls
[call(1), call(5, 'hello', a=True)]
Each element of the returned array is a call
object, which stores the arguments for
each call.
name, args, kwargs = function.mock_calls[1]
args, kwargs
((5, 'hello'), {'a': True})
Many real-world objects would contain some sort of state, which would change as the
object is used via side-effects. Mock
objects can also be setup to reproduce this
behaviour like so:
function = Mock(name="myroutine", side_effect=[2, "xyz"])
function(1)
2
function(1, "hello", {'a': True})
'xyz'
Since we have only specified two side-effects for our mock object, we therefore expect an error if the function is called again:
function()
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-10-2fcbbbc1fe81> in <module>()
----> 1 function()
C:\ProgramData\Anaconda3\lib\unittest\mock.py in __call__(_mock_self, *args, **kwargs)
917 # in the signature
918 _mock_self._mock_check_sig(*args, **kwargs)
--> 919 return _mock_self._mock_call(*args, **kwargs)
920
921
C:\ProgramData\Anaconda3\lib\unittest\mock.py in _mock_call(_mock_self, *args, **kwargs)
976
977 if not _callable(effect):
--> 978 result = next(effect)
979 if _is_exception(result):
980 raise result
StopIteration:
This demonstrates a couple of the features of unittest.Mock
to give you a flavor of
how it can be used to create a mock object. There are of course many other features of
the library, for example the ability to create mock methods and attributes for your mock
object. You can read more about these in the
documentation.
Using mocks to model test resources
Often we want to write tests for code which interacts with remote resources. (e.g. databases, the internet, or data files)
We don’t want to have our tests actually interact with the remote resource, as this would mean our tests failed due to lost internet connections, for example.
Instead, we can use mocks to assert that our code does the right thing in terms of the messages it sends: the parameters of the function calls it makes to the remote resource.
For example, consider the following code that downloads a map from the internet using the Google API:
import requests
def map_at(lat, long, satellite=False, zoom=12,
size=(400, 400), sensor=False):
base = "http://maps.googleapis.com/maps/api/staticmap?"
params = dict(
sensor = str(sensor).lower(),
zoom = zoom,
size = "x".join(map(str,size)),
center = ",".join(map(str,(lat,long))),
style = "feature:all|element:labels|visibility:off")
if satellite:
params["maptype"] = "satellite"
return requests.get(base, params=params)
london_map = map_at(51.5073509, -0.1277583)
from IPython.display import Image
We would like to write a test to check that the map_at
function is building the
parameters correctly for the call to the google mapping API. We can do this by mocking
the requests object. Note that we could simply put a print
statement in the function
to verify this, but this would have to be removed, so writing a test for this is a more
long-term and sustainable solution.
We need to temporarily replace a method, specifically the requests.get()
function, with
a mock. We can use unittest.mock.patch
to do this:
from unittest.mock import patch
with patch.object(requests,'get') as mock_get:
london_map = map_at(51.5073509, -0.1277583)
print(mock_get.mock_calls)
The patch.object
function replaces the named attribute (in this case get
) in the
named object (requests
), with a mock object mock_get
. In the same manner as we saw
for the Mock
objects, we can query mock_get
to get the arguments that requests.get
was called with.
[call('http://maps.googleapis.com/maps/api/staticmap?', params={'center': '51.5073509,-0.1277583', 'sensor': 'false', 'style': 'feature:all|element:labels|visibility:off', 'size': '400x400', 'zoom': 12})]
Our test for the map_at
function then looks like:
def test_build_default_params():
with patch.object(requests,'get') as mock_get:
default_map = map_at(51.0, 0.0)
mock_get.assert_called_with(
"http://maps.googleapis.com/maps/api/staticmap?",
params={
'sensor':'false',
'zoom':12,
'size':'400x400',
'center':'51.0,0.0',
'style':'feature:all|element:labels|visibility:off'
}
)
unittest.patch
can be used as a function
decorator to
replace an argument of the function with a mock, it can used to patch a object
attribute
, a
dictionary-like
object, or to apply
multiple
patches as once.
Here is the same test for map_at
, now written using a patch
function decorator.
@patch('requests.get')
def test_build_default_params(mock_get):
default_map = map_at(51.0, 0.0)
mock_get.assert_called_with(
"http://maps.googleapis.com/maps/api/staticmap?",
params={
'sensor':'false',
'zoom':12,
'size':'400x400',
'center':'51.0,0.0',
'style':'feature:all|element:labels|visibility:off'
}
)
Testing functions that call other functions
Often we wish to test a function in isolation from other, dependent functions, so that we can write independent tests. For example, we want to test that the below function does the right thing. It is supposed to compute the derivative of a function of a vector in a particular direction.
def partial_derivative(function, at, direction, delta=1.0):
f_x = function(at)
x_plus_delta = at[:]
x_plus_delta[direction] += delta
f_x_plus_delta = function(x_plus_delta)
return (f_x_plus_delta - f_x) / delta
We can us the partial_derivative
function to, for example, calculate the derivative of
the sum
function.
partial_derivative(sum, [0,0,0], 1)
1.0
How do we assert that it is doing the right thing? One way we could do this is to check
the result of the function against the correct solution. After all, we know that the
partial derivative of the sum
function should be 1, we could also check the
performance of the function using other test functions as well. But with mocks we can
also write a test that is completely independent of the specific function we might use,
and one that directly tests that the partial_derivative
is doing what you expect it to
do. Here is a possible test using a Mock
to replace the input function
argument.
from unittest.mock import Mock
def test_derivative_2d_y_direction():
func = Mock(return_value=0)
partial_derivative(func, [0,0], 1)
func.assert_any_call([0, 1.0])
func.assert_any_call([0, 0])
test_derivative_2d_y_direction()
Write a unit test for
load_csv
using mocksGo back to the
inflammation
example that we used in the previous lessons.Write a unit test for the
load_csv
function ininflammation/models.py
. Use mocking to remove the dependence on thenp.loadtxt
andinflammation.models.get_data_dir
functions. Make sure to test for the two different behaviours of the function which occur when the input filename is either absolute or relative.Once you are finished, commit your changes and then merge all the new commits on your
test-suite
branch into `master.Solution
... @patch('inflammation.models.get_data_dir', return_value='/data_dir') def test_load_csv(mock_get_data_dir): from inflammation.models import load_csv with patch('numpy.loadtxt') as mock_loadtxt: load_csv('test.csv') name, args, kwargs = mock_loadtxt.mock_calls[0] assert kwargs['fname'] == '/data_dir/test.csv' load_csv('/test.csv') name, args, kwargs = mock_loadtxt.mock_calls[1] assert kwargs['fname'] == '/test.csv' ...
Key Points
Mocking is useful for implementing unit tests that test each component in isolation.
Mocking can remove dependencies on external environment such as the internet, the filesystem, or it can remove dependencies from other components
Testing in isolation is essential as it gives you a test suite that can properly identify where bugs are occuring