Joe Ray

Software and Infrastructure Development.

Mocking files and file storage for unit testing Django models

When writing unit tests, avoiding interaction with external services (for example the filesystem or network) is considered best practice because you can’t rely on external services’ availability, nor do they present a consistent, predictable state when running your tests. Normally, the way to achieve this is to mock out the particular service your code depends upon but when your code uses a large framework such as Django it becomes difficult to ascertain what to mock and how to mock it.

In my case, I was trying to unit test some code which interacted with a Model with a FileField on it. To create an instance of the Model I needed to populate the FileField with a value but the field expects a django.core.files.File instance.

This presents two problems:

  1. If I give the Model an actual File instance I need a file to read which would involve touching the filesystem
  2. Upon saving the Model the file will be processed and stored using the file storage system, again touching the filesystem (or the network if you have a custom storage system to e.g. upload to S3)

My solution involved mocking both the File instance and the file storage system. For my unit tests, I use the Python library mock.

First we need to mock the File instance. There is only one thing we really need to mock – the name attribute – because it is primarily the file storage system which accesses the File’s methods:

import mock
from django.core.files import File

file_mock = mock.MagicMock(spec=File, name='FileMock')
file_mock.name = 'test1.jpg'

This tells the mock library to create a mock object based on Django’s File class and assigns the name attribute. Using this is as simple as assigning the mock to the file attribute on our model:

asset = Asset()
asset.file = file_mock

The file storage system is a little more tricky. We need to create the mock and then get Django to use it. Luckily there’s one way in which Django retrieves the default file storage system. We can use mock’s patch function to force it to return our mock instead of the default:

from django.core.files.storage import Storage

storage_mock = mock.MagicMock(spec=Storage, name='StorageMock')
storage_mock.url = mock.MagicMock(name='url')
storage_mock.url.return_value = '/tmp/test1.jpg'

with mock.patch('django.core.files.storage.default_storage._wrapped', storage_mock):
    # The asset is saved to the database but our mock storage
    # system is used so we don't touch the filesystem
    asset.save()

We need to patch _wrapped because default_storage is a lazy-loaded object and _wrapped is the property which it uses to determine if it’s been loaded yet or not. Note: this won’t work if you’re manually setting the storage property of the file field when creating the Model; you must be relying on the DEFAULT_FILE_STORAGE setting.

Edit: A previous version of this article recommended patching get_storage_class. @Paul_Collins spotted that this would lead to Django caching the first test’s mock rather than replacing each time. This new method of patching _wrapped should get around that.

Contact Me

I love a good conversation! E-mail me (encrypted if you want) or find me on Twitter, LinkedIn and GitHub.