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 = ''

    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 your project module, next to the and the root 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.

Download Shipping Faster: Django Team Improvements
blog comments powered by Disqus