Python: mock reading and writing files
Since Python 3.3, the standard library has included unitest.mock
, which provides utilities for mocking out parts of the system for testing.
These notes cover how to use unittest.mock
when reading from one or more files, or writing to a single file.
Mock reading a single file §
Using the patch
context manager, we mock out all calls to open(...)
(i.e. "builtins.open"
) within that block.
- By using the special mock object
mock_open(read_data=...)
, we can specify what data will be returned whenreadlines()
is called. - By keeping a reference to the mock object (
m
), we can query which arguments were used in the call toopen(...)
.
As an example:
These examples are written for running under pytest, but the body of the test function should work just the same regardless of test framework. No pytest specific functionality is used for the mocking, only the Python standard library.
Mock reading multiple files §
A limitation of the previous example is that all calls to open(...)
within that block are mocked out with the same mock object. This is fine if you are only mocking one call to open(...)
, but if you have multiple calls, you need each one to be assigned a different mock_open()
object.
To support mocking multiple different calls to open(...)
, we need to supply a separate mock_open()
object depending upon which filename is being requested:
- First, create a
filename_to_mock_open()
function. This takes afilename
and returns amock_open(...)()
. Note the additional parentheses. - Next, create a
mock_open_multiple(on_open=...)
function. It takes one argument (on_open
), which is the function to execute instead ofopen()
.
Putting it all together, we get this:
You may want to organise things differently:
- The
mock_open_multiple()
function is generic, and could be added to a set of common utility functions for testing. - The
filename_to_mock_open()
function is specific to code under test.
Mock writing a single file §
We can mock writing to a file by using patch
to replace open()
with a mock object (m
), and then we can assert that the mock object is called as expected:
Note that, because we use a context manager for writing (e.g. with open(filename, "w")
), all the calls to write occur within that context (e.g. call().__enter__().write(...)
).
If the order does not matter, use assert_has_calls(..., any_order=True)
.
Conclusion §
Python makes it easy to mock out files for testing, but it’s even easier if you don’t need to mock in the first place.
As a rule of thumb, keep your functions that read/write to files as simple as possible, and place the complex logic in separate functions that can be tested with minimal or zero mocking.