Mocking objects with Python
Mocking is just pretending to be and simulating specific behavior, we can replace our code’s parts with fake objects and verify how system behaves. At some point our system’s code starts to be complicated and have so many dependencies what makes it impossible to test without mocks. To cover all corner cases or to freely control our test scenario flow we have to simulate part of functionality – we have to force it to behave in exact way we want. For this purpose python uses unitest.mock library which is a standard library starting from Python 3.3 (for older versions installation is required).
Patching
Patching is basically changing something into its mock equivalent. Patching can be done as:
- import patching, where import statement represented by a string given to patch function is replaced with a mock object;
- object patching, where specific object instance is replaced with a mock
and in both cases the return value can be set. Patch function can be used as a context manager or a method decorator.
Let’s write a simple class FileMonitor with a method we want to test. Method will make os call – checking if file exists.
import os
class FileMonitor:
def __init__(self, file_name):
self.file_name = file_name
def check_file(self):
if not os.path.isfile(self.file_name):
raise FileNotFoundError(f'{self.file_name} not found')
Testing this method is of course possible just as it is but requires creating or copying file from some directory before test is executed. It is maybe not very difficult but may behave differently under different operating system and additionally it is hard to check whether actual os call was made. So the test would be like:
from unittest import TestCase
from jlabs.file import FileMonitor
class FileTest(TestCase):
def test_file_monitor_01(self):
# Handle file creation or copying
monitor = FileMonitor('file')
monitor.check_file()
# not really possible to check if os call was made
Summing up – it is not as simple as it supposes to be and not everything is checked as we would like. Here mocks can help as and simplify our case. Sa let’s modify our test and add a mock:
from unittest import TestCase, mock
from jlabs.file import FileMonitor
class FileTest(TestCase):
def test_file_monitor_01(self):
with mock.patch('os.path.isfile', return_value=True) as mocked_os:
monitor = FileMonitor('file')
monitor.check_file()
self.assertEqual(1, mocked_os.call_count)
# or
mocked_os.assert_called_once()
mocked_os.assert_called_once_with('file')
Let’s see what was done here. We used mock.patch() method, where we put an import’s namespace we want to patch plus its return value and we are given a mock object which gives us possibility to check some useful information (e.g. call count). Patching was used as a context manager. We should also add another test to check opposite return value (in this case Error shall be raised):
def test_file_monitor_02(self):
with mock.patch('os.path.isfile', return_value=False) as mocked_os, self.assertRaises(FileNotFoundError):
monitor = FileMonitor('file')
monitor.check_file()
self.assertEqual(1, mocked_os.call_count)
The alternative for context manager is to used patching as test decorators – the difference is that such test method would have additional parameter which will be our mock object. So, the first test converted to use decorator will look like:
@mock.patch('os.path.isfile', return_value=True)
def test_file_monitor_03(self, mocked_os):
monitor = FileMonitor('file')
monitor.check_file()
self.assertEqual(1, mocked_os.call_count)
mocked_os.assert_called_once_with('file')
Another option for patching is object patching – here we are mocking specific class’s attribute, like method.
Let’s add another method to FileMonitor class:
Another option for patching is object patching – here we are mocking specific class’s attribute, like method.
Let’s add another method to FileMonitor class:
And now we will create simple Utility class using FileMonitor class
class Utility:
def __init__(self):
self.monitor = FileMonitor('some_file')
def go(self):
return 'directory' if self.monitor.is_directory() else 'file'
In order to make test checking Utility class without actual os calls we have to patch FileMonitor class using mock.patch.object function, where we pass class to patch, method name (as string) and overwritten method (‘new’ argument). Test will look like:
def test_utility(self):
with mock.patch.object(FileMonitor, FileMonitor.is_directory.__name__, new=lambda cls: True):
utility = Utility()
self.assertEqual('directory', utility.go())
Method is_directory was replaced with simple lambda returning True. Another way to achieve it is to operate on mock object and assign return value according to our needs:
@mock.patch.object(FileMonitor, FileMonitor.is_directory.__name)
def test_utility_02(self, mocked_monitor):
mocked_monitor.return_value = False
utility = Utility()
self.assertEqual('file', utility.go())
Mock objects
We were mentioning mock objects in last chapter without describing what is it. We can find two mock classes in unittest library – Mock and MagicMock. It’s objects create all methods and attributes when being accessed and keep information about its usage. So, the mocks returned during patching are Mock objects (MagicMock actually which is a subclass of Mock). Let’s look at last example and change this test so it explicitly uses mock classes.
def test_utility_03(self,):
utility = Utility()
utility.monitor.is_directory = Mock(return_value=True)
self.assertEqual('directory', utility.go())
utility.monitor.is_directory.assert_called_with()
We replaced is_directory() method to be Mock object with specified returned value and after all we can access this object and check how it was used – in our case we are checking if it was called without any argument. There much more we can do with mock objects for example set a side effect when calling a mock:
def test_utility_04(self,):
utility = Utility()
utility.monitor.is_directory = Mock(side_effect=FileNotFoundError('not_found'))
with self.assertRaises(FileNotFoundError):
utility.go()
utility.monitor.is_directory.assert_called()
At the beginning of this chapter I mentioned that there are two mock classes. Why do we need two and which one shall we use? I already said MagicMock inherits from Mock class and implements some ‘magic’ methods that need to be implemented by us in Mock class, e.g.:
Methods that are preconfigured in MagicMock class are:
- __int__: 1
- __contains__: False
- __len__: 0
- __iter__: iter([])
- __float__: 1.0
- __bool__: True
Full list can be checked in Python documentation.
Services
Last chapter I would like to dedicate services testing. It is not a trivial case because it always requires external component (client or server) which triggers request or responds to request with a specific response. Testing HTTP server is simple because it only requires preparing a correct request and verify response – we can use simple requests library in Python for this:
from unittest import TestCase
import requests
class ServicesTest(TestCase):
def test_services_01(self):
credentials = ('user', 'password')
url = 'http://<url_we want_to_test>/resource'
response = requests.get(url=url, auth=credentials)
self.assertEqual(200, response.status_code)
This is a simple test checking if our HTTP server responds correctly to given message. We can access all fields in response structure depending what we would check. More complicated is testing HTTP client, because it forces us to run a mocked service and prepare fake responses to client’s requests. The most popular library for this purpose working across many languages (including Python) is wiremock. First step to launch wiremock server is to define so called mappings – we have to tell server how does it suppose to respond (body, code, etc.) to given (URL, method) request. Then we have to configure URL, where the server stands and we are ready to go.
from unittest import TestCase
import requests
from wiremock.constants import Config
from wiremock.resources.mappings import Mapping, MappingRequest, HttpMethods, MappingResponse
from wiremock.resources.mappings.resource import Mappings
from wiremock.server import WireMockServer
class ServicesTest(TestCase):
def test_services_02(self):
mapping = Mapping(
priority=100,
request=MappingRequest(
method=HttpMethods.GET,
url='/hello'
),
response=MappingResponse(
status=200,
body='world'
),
persistent=False)
with WireMockServer() as wm:
Config.base_url = f'http://localhost:{wm.port}/__admin'
Mappings.create_mapping(mapping)
response = requests.get(f'http://localhost:{wm.port}/hello')
self.assertEqual(b'world', response.content)
As you can see we started with defining the mapping – once server gets GET request sent to /hello URL it will respond with ‘200’ message with a string ‘world’ in body’s content. Then we start WireMockServer (it draws first available port), configure URL and add mapping created before. Then requests library from first example is used to test the server.
Summary
That is all I prepared to cover the topic of mocking in a nutshell. If you find this subject interesting I strongly recommend looking into documentation to find additional information that may help you write you test scenarios. Libraries I showed you are the most popular in python for this purpose and knowing them will definitely give you solid foundations for testing your code.