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
This presents two problems:
- If I give the Model an actual File instance I need a file to read which would involve touching the filesystem
- 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
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
Edit: A previous version of this article recommended patching
spotted that this would lead to Django caching the first test’s mock rather than replacing each time. This new method of
_wrapped should get around that.