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:
- 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 _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.