In my previous article, I demonstrated how to create a unit test for a Python function that raises an exception. This allows a function to be tested without its execution being fatal.
Another case where a similar pattern can be applied is when mocking a function.
unittest.mock
is a standard library module for testing in
Python. It allows you to replace parts of your system under test with mock
objects and make assertions about how they have been used.
In this example, we leverage the patch()
function, which handles
patching module- and class-level attributes within the scope of a test. When
using patch()
, the object you specify will be replaced with a mock (or other
object) during the test and restored when the test ends.
Problem
Let’s say I have a function open_json_file()
:
def open_json_file(filename):
"""
Attempt to open and deserialize a JSON file.
:param filename: name of the JSON file
:type filename: str
:return: dict of log
:rtype: dict
"""
try:
with open(filename) as f:
try:
return json.load(f)
except ValueError:
raise ValueError('{} is not valid JSON.'.format(filename))
except IOError:
raise IOError('{} does not exist.'.format(filename))
How do I mock open()
to prevent it from being called with the actual file?
How do I elicit an IOError
and/or ValueError
and test the exception
message?
Solution
To test this behavior, we use mock_open()
in conjunction
with assertRaises()
. mock_open()
is a helper function
to create a mock to replace the use of the built-in function open()
.
assertRaises()
allows an exception to be encapsulated, which means that the
test can raise and inspect exceptions without halting execution, as would
normally happen with unhandled exceptions. By nesting both patch()
and
assertRaises()
we can simulate the data returned by open()
and cause an
exception to be raised.
The first step is to create the MagicMock
object:
read_data = json.dumps({'a': 1, 'b': 2, 'c': 3})
mock_open = mock.mock_open(read_data=read_data)
NOTE: read_data
is the string that will be returned by the file handle’s
read()
method. This is an empty string by default.
Next, using patch()
as a context manager, open()
can be patched with the
new object, mock_open
:1
with mock.patch('__builtin__.open', mock_open):
...
Within this context, a call to open()
returns mock_open
, the MagicMock
object:
with mock.patch('__builtin__.open', mock_open):
with open(filename) as f:
...
In the case of our example:
with mock.patch('__builtin__.open', mock_open):
result = open_json_file('filename')
With assertRaises()
as a nested context, we can then test for the raised
exception when the file does not contain valid JSON:
read_data = ''
mock_open = mock.mock_open(read_data=read_data)
with mock.patch('__builtin__.open', mock_open):
with self.assertRaises(ValueError) as context:
open_json_file('filename')
self.assertEqual(
'filename is not valid JSON.', str(context.exception))
Here is the full test, which provides full test coverage of the original
open_json_file()
function:
def test_open_json_file(self):
# Test valid JSON.
read_data = json.dumps({'a': 1, 'b': 2, 'c': 3})
mock_open = mock.mock_open(read_data=read_data)
with mock.patch('__builtin__.open', mock_open):
result = open_json_file('filename')
self.assertEqual({'a': 1, 'b': 2, 'c': 3}, result)
# Test invalid JSON.
read_data = ''
mock_open = mock.mock_open(read_data=read_data)
with mock.patch('__builtin__.open', mock_open):
with self.assertRaises(ValueError) as context:
open_json_file('filename')
self.assertEqual(
'filename is not valid JSON.', str(context.exception))
# Test file does not exist.
with self.assertRaises(IOError) as context:
open_json_file('null')
self.assertEqual(
'null does not exist.', str(context.exception))
In Python 3, patch
builtins.open
instead of__builtin__.open
. ↩︎