MEDIA_ROOT and Django Tests
If you’ve ever written a test for a view or model with associated
uploaded files you might have noticed a small problem with those files
hanging around after the tests are complete. Since version 1.3, Django
won’t delete the files associated with your model instances when they
are deleted. Some work-arounds for this
issue
involve writing a custom delete for your model or using a post_delete
signal handler. But even with those in place the files would not be
deleted during tests because the model instances are not explicitly
deleted at the end of the test case. Instead, Django simply rolls back
the transaction and the delete method is never called nor are the
signals fired. This can be quite an annoyance when running the tests
repeatedly and watching your MEDIA_ROOT
(or worse your S3 bucket) fill
up with garbage data. More than annoyance, this introduces something you
always want to avoid in unittests: global state.
One way to avoid this problem is to take a similar approach to how
Django manages the database. That is, each time you run the tests,
create a new MEDIA_ROOT
and when the tests finish tear it down. This
can all be done with a basic test runner class.
import shutil
import tempfile
from django.conf import settings
from django.test.simple import DjangoTestSuiteRunner
class TempMediaMixin(object):
"Mixin to create MEDIA_ROOT in temp and tear down when complete."
def setup_test_environment(self):
"Create temp directory and update MEDIA_ROOT and default storage."
super(TempMediaMixin, self).setup_test_environment()
settings._original_media_root = settings.MEDIA_ROOT
settings._original_file_storage = settings.DEFAULT_FILE_STORAGE
self._temp_media = tempfile.mkdtemp()
settings.MEDIA_ROOT = self._temp_media
settings.DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
def teardown_test_environment(self):
"Delete temp storage."
super(TempMediaMixin, self).teardown_test_environment()
shutil.rmtree(self._temp_media, ignore_errors=True)
settings.MEDIA_ROOT = settings._original_media_root
del settings._original_media_root
settings.DEFAULT_FILE_STORAGE = settings._original_file_storage
del settings._original_file_storage
class CustomTestSuiteRunner(TempMediaMixin, DjangoTestSuiteRunner):
"Local test suite runner."
The current Django master (1.6 dev) has a new discovery test runner. You can also make this mixin work with this new runner.
## Requires Django 1.6+
from django.test.runner import DiscoverRunner
class CustomTestSuiteRunner(TempMediaMixin, DiscoverRunner):
"Local test suite runner."
This code can go anywhere on your Python import path. You might choose
to put this into a runner.py
your project module, next to the
settings.py
and the root urls.py
. In that case you would update your
settings with
## Here {{ project_name }} would be replaced by the name of your project module
TEST_RUNNER = '{{ project_name }}.runner.CustomTestSuiteRunner'
This is written as a mixin so that you could add the same functionality
to an existing test runner such as the one provided by django-jenkins,
django-discovery-runner or django-celery. While this doesn’t entirely
avoid the global state between tests, it does avoid problems between
test runs and keeps the tests from filling up your local MEDIA_ROOT
.