XClose

COMP0233: Research Software Engineering With Python

Home
Menu

Mocking

Definition

Mock: verb,

  1. to tease or laugh at in a scornful or contemptuous manner
  2. to make a replica or imitation of something

Mocking

  • Replace a real object with a pretend object, which records how it is called, and can assert if it is called wrong

Mocking frameworks

Recording calls with mock

Mock objects record the calls made to them:

In [1]:
from unittest.mock import Mock
function = Mock(name="myroutine", return_value=2)
In [2]:
function(1)
Out[2]:
2
In [3]:
function(5, "hello", a=True)
Out[3]:
2
In [4]:
function.mock_calls
Out[4]:
[call(1), call(5, 'hello', a=True)]

The arguments of each call can be recovered

In [5]:
name, args, kwargs = function.mock_calls[1]
args, kwargs
Out[5]:
((5, 'hello'), {'a': True})

Mock objects can return different values for each call

In [6]:
function = Mock(name="myroutine", side_effect=[2, "xyz"])
In [7]:
function(1)
Out[7]:
2
In [8]:
function(1, "hello", {'a': True})
Out[8]:
'xyz'

We expect an error if there are no return values left in the list:

In [9]:
function()
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
Cell In[9], line 1
----> 1 function()

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/unittest/mock.py:1139, in CallableMixin.__call__(self, *args, **kwargs)
   1137 self._mock_check_sig(*args, **kwargs)
   1138 self._increment_mock_call(*args, **kwargs)
-> 1139 return self._mock_call(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/unittest/mock.py:1143, in CallableMixin._mock_call(self, *args, **kwargs)
   1142 def _mock_call(self, /, *args, **kwargs):
-> 1143     return self._execute_mock_call(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/unittest/mock.py:1200, in CallableMixin._execute_mock_call(self, *args, **kwargs)
   1198     raise effect
   1199 elif not _callable(effect):
-> 1200     result = next(effect)
   1201     if _is_exception(result):
   1202         raise result

StopIteration: 

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:

In [10]:
# sending requests to the web is not fully supported on jupyterlite yet, and the
# cells below might error out on the browser (jupyterlite) version of this notebook
import requests

def map_at(lat, long, satellite=False, zoom=12, 
           size=(400, 400)):

    base = "https://static-maps.yandex.ru/1.x/?"
    
    params = dict(
        z = zoom,
        size = ",".join(map(str,size)),
        ll = ",".join(map(str,(long,lat))),
        lang = "en_US")
    
    if satellite:
        params["l"] = "sat"
    else:
        params["l"] = "map"
        
    return requests.get(base, params=params)
In [11]:
london_map = map_at(51.5073509, -0.1277583)
from IPython.display import Image
In [12]:
%matplotlib inline
Image(london_map.content)
Out[12]:
No description has been provided for this image

We would like to test that it is building the parameters correctly. We can do this by mocking the requests object. We need to temporarily replace a method in the library with a mock. We can use "patch" to do this:

In [13]:
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)
[call('https://static-maps.yandex.ru/1.x/?', params={'z': 12, 'size': '400,400', 'll': '-0.1277583,51.5073509', 'lang': 'en_US', 'l': 'map'})]

Our tests then look like:

In [14]:
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(
        "https://static-maps.yandex.ru/1.x/?",
        params={
            'z':12,
            'size':'400,400',
            'll':'0.0,51.0',
            'lang':'en_US',
            'l': 'map'
        }
    )
test_build_default_params()

That was quiet, so it passed. When I'm writing tests, I usually modify one of the expectations, to something 'wrong', just to check it's not passing "by accident", run the tests, then change it back!

Testing functions that call other functions

In [15]:
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 want to test that the above function does the right thing. It is supposed to compute the derivative of a function of a vector in a particular direction.

E.g.:

In [16]:
partial_derivative(sum, [0,0,0], 1)
Out[16]:
1.0

How do we assert that it is doing the right thing? With tests like this:

In [17]:
from unittest.mock import MagicMock

def test_derivative_2d_y_direction():
    func = MagicMock()
    partial_derivative(func, [0,0], 1)
    func.assert_any_call([0, 1.0])
    func.assert_any_call([0, 0])
    

test_derivative_2d_y_direction()

We made our mock a "Magic Mock" because otherwise, the mock results f_x_plus_delta and f_x can't be subtracted:

In [18]:
MagicMock() - MagicMock()
Out[18]:
<MagicMock name='mock.__sub__()' id='139993165587344'>
In [19]:
Mock() - Mock()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[19], line 1
----> 1 Mock() - Mock()

TypeError: unsupported operand type(s) for -: 'Mock' and 'Mock'