Skipping Test DB Creation
We are always looking for ways to make our tests run faster. That means
writing tests which don't preform I/O (DB reads/writes, disk
reads/writes) when possible. Django has a collection of TestCase
subclasses
for different use cases. The common TestCase
handles the fixture
loading and the creation the of TestClient
. It uses the database
transactions to ensure that the database state is reset for every test.
That is it wraps each test in a transaction and rolls it back once the
test is over. Any transaction management inside the test becomes a
no-op. Since [TestCase]{.title-ref}[ overrides the transaction
facilities, if you need to test the transactional behavior of a piece of
code you can instead use
]{.title-ref}[TransactionTestCase]{.title-ref}[.
]{.title-ref}[TransactionTestCase]{.title-ref}` resets the database
after the test runs by truncating all tables which is much slower than
rolling back the transaction particularly if you have a large number of
tables.
There is also SimpleTestCase
which is the base class for the previous
to classes. It has some additional assertions for testing HTML and
overriding Django settings but doesn't manage the database state. If
you are testing something that doesn't need to interact with the
database such as form field/widget output, utility code or have mocked
all of the database interactions you can save the overhead of the
transaction by using SimpleTestCase
.
Now what if you are running a set of tests which are only using
SimpleTestCase
or the base unittest.TestCase
? Then you don't really
need the test database creation at all. Depending on the backend you are
using, the number of tables you have and the number of tests you are
running the database creation can take many times longer than running
the test themselves.
Our solution for this was to extend the default test runner. A quick examination of the build-in test runner reveals a solution.
def run_tests(self, test_labels, extra_tests=None, **kwargs):
"""
Run the unit tests for all the test labels in the provided list.
Test labels should be dotted Python paths to test modules, test
classes, or test methods.
A list of 'extra' tests may also be provided; these tests
will be added to the test suite.
Returns the number of tests that failed.
"""
self.setup_test_environment()
suite = self.build_suite(test_labels, extra_tests)
old_config = self.setup_databases()
result = self.run_suite(suite)
self.teardown_databases(old_config)
self.teardown_test_environment()
return self.suite_result(suite, result)
The test suite is discovered before the test db is created. That means
we can look at the set of tests which are going to be run and if none of
them are using TransactionTestCase
(TestCase
is a subclass of
TransactionTestCase
) then we can skip the database creation/teardown.
Here's what that looks like:
from django.test import TransactionTestCase
try:
from django.test.runner import DiscoverRunner as BaseRunner
except ImportError:
# Django < 1.6 fallback
from django.test.simple import DjangoTestSuiteRunner as BaseRunner
from mock import patch
class NoDatabaseMixin(object):
"""
Test runner mixin which skips the DB setup/teardown
when there are no subclasses of TransactionTestCase to improve the speed
of running the tests.
"""
def build_suite(self, *args, **kwargs):
"""
Check if any of the tests to run subclasses TransactionTestCase.
"""
suite = super(NoDatabaseMixin, self).build_suite(*args, **kwargs)
self._needs_db = any([isinstance(test, TransactionTestCase) for test in suite])
return suite
def setup_databases(self, *args, **kwargs):
"""
Skip test creation if not needed. Ensure that touching the DB raises and
error.
"""
if self._needs_db:
return super(NoDatabaseMixin, self).setup_databases(*args, **kwargs)
if self.verbosity >= 1:
print 'No DB tests detected. Skipping Test DB creation...'
self._db_patch = patch('django.db.backends.util.CursorWrapper')
self._db_mock = self._db_patch.start()
self._db_mock.side_effect = RuntimeError('No testing the database!')
return None
def teardown_databases(self, *args, **kwargs):
"""
Remove cursor patch.
"""
if self._needs_db:
return super(NoDatabaseMixin, self).teardown_databases(*args, **kwargs)
self._db_patch.stop()
return None
class FastTestRunner(NoDatabaseMixin, BaseRunner):
"""Actual test runner sub-class to make use of the mixin."""
There are a couple of things to note. Like the previous temporary
MEDIA_ROOT
runner,
this is written as a mixin so that it can be combined with other test
runner improvements. Second it uses
mock to ensure that any attempts to
connect to the database will fail. This idea is borrowed from Carl
Meyer's Testing and Django
Talk from PyCon 2012.
To make use of this you would need to include this runner on your Python
path and change the TEST_RUNNER
setting to the full Python path to the
FastTestRunner
class.
With this in place if you had the follow tests
## myapp.tests.py
from django.test import TestCase, SimpleTestCase
class DbTestCase(TestCase):
"""Does something with the DB."""
class NoDbTestCase(SimpleTestCase):
"""Does something with the DB."""
in your app called myapp
. If you were to run:
python manage.py test myapp
it would create the DB. However, if you run:
# For Django < 1.6
python manage.py test myapp.NoDbTestCase
# For Django 1.6+ with the DiscoverRunner
python manage.py test myapp.tests.NoDbTestCase
it would skip the test DB creation. Hooray for faster tests!