How to Mock the Built-in Function open()

March 2018 · 3 minutes

In my last article, I discussed how to create a unit test for a Python function that raises an exception. This technique enables a function to be called with parameters that would cause an exception without its execution being fatal.

Another scenario in which a similar pattern can be applied is when mocking a function. mock is a library 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 will 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

For example, 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 so that it is not called with the passed argument? How do I elicit a IOError and/or ValueError and test the exception message?

Solution

The solution is to 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 throw an exception without exiting execution, as is normally the case for unhandled exceptions. By nesting both patch and assertRaises we can fib about the data returned by open and cause an exception is 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 a string for the ~io.IOBase.read method of the file handle to return. This is an empty string by default.

Next, using patch as a context manager, open can be patched with the new object, mock_open:

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 100% 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))