nblock's ~

Mocking open() in Python 3 unit tests

I use flask quite a lot for several projects and over the last months my primary goal was to improve the code coverage with unit tests. There, I came across several occasions where the builtin open() is used in a flask view. A file is opened, its content gets read and the results are passed on to a template. The following minimal example shows a typical usage:

from flask import Flask

app = Flask("MyFlaskApp")

def index():
        with open("/no/such/file/i/guess", 'r') as f:
            content = f.read()
    except FileNotFoundError:
        content = "No such thing"
    return content

When it comes to unit testing, the mock library is quite useful. As a side note, since Python 3.3, mock is shipped as part of Python's unittest library. It also provides a helper function, mock_open() to easily mock calls to open(). The usual code snippets out there mock calls to open() from (the global) builtins. This causes issues when open() should only be mocked for a particular module and stay untouched for all other modules. The following test piece may be used to mock out the call to open() in the context of the flask module to test. All other calls to open() stay untouched.

from mymodule import app
import unittest
from unittest.mock import mock_open, patch

class MyModule(unittest.TestCase):

    def setUp(self):
        self.app = app.test_client()

    def test_no_such_thing_on_file_not_found_error(self):
        m = mock_open()
        m.side_effect = FileNotFoundError()
        with patch('mymodule.open', m, create=True):
            rv = self.app.get('/')
            self.assertTrue("No such thing", rv.data)
        m.assert_called_once_with("/no/such/file/i/guess", "r")

The important piece of the above snippet is create=True, which is documented as follows:

By default patch() will fail to replace attributes that don’t exist. If you pass in create=True, and the attribute doesn’t exist, patch will create the attribute for you when the patched function is called, and delete it again afterwards. This is useful for writing tests against attributes that your production code creates at runtime. It is off by default because it can be dangerous. With it switched on you can write passing tests against APIs that don’t actually exist!

Feel free to download the example code and run the tests with:

python3 -m unittest discover

Serve the application:

python3 -c 'from mymodule import app; app.run()'

Feedback? Contact me!


tagged flask, mock, python3 and unittest