Django is a strong framework for building reliable websites. However, it is showing signs of age. Particularly, it is failing where tools such as Spring Boot excel.
This article examines how to overcome yet another issue encountered in Django, running tests without migrating databases.
There are many times when a database migration is inappropriate for testing:
- we have separated test, development, and production databases with custom features requiring testing
- there are fixtures pre-loaded into a database
- we have legwork to complete through scripts alongside database setup
- dropping and re-creating a database eliminates data that is absolutely necessary to another function in our test environment
- nothing we do can be included in a TestRunner, at least not reliably
Sadly, Django developers, among other issues they refuse to fix, were not willing to create a workaround until recently. The workaround does not work in the latest version.
Django Unit Test Framework
The Django unit test framework comes with many features typically reserved for tools such as ghost.py or selenium.
For instance, Django provides:
from django.test import TestCase from django.test import Client from django.test import RequestFactory
A RequestFactory is used to perform specific tests while the Client acts as a browser would.
While there is an option to use pytest, the changing nature of Django manages to continually break this tool. Importing models from different applications using multiple databases is a broken process at the time of publication.
Django provides an extensive tutorial for setting up unit tests. The setup is straightforward:
from django.test import TestCase from django.test import Client from django.test import RequestFactory class AdminTestCases(TestCase): multi_db = True def setUp(self): self.client = Client() self.rf = RequestFactory() def test_create_superuser(self): pass def test_remove_superuser(self): pass def test_fail_create_superuser(self): pass def test_fail_remove_superuser(self): pass def tearDown(self): pass
This skeleton prepares a client and request factory, generates tests, and provides a tear down function. Setup and tear down appear to run before and after each test.
For security and other purposes, it is often necessary to separate databases. This offers a degree of protection for sensitive information.
Unfortunately, this also presents an issue with the tests in which a single database is used. To avoid this, each test case must include the following:
multi_db = True
This line informs the the test runner to search for different databases.
Ignoring Migrated Databases
Now, for the meat and potatoes. We want to ignore database migration altogether at times. It is still wise to have a script and drop any unused data.
Django allows for the generation of custom TestRunner classes. As of Django > 2.0. the following runner avoids migrating a database:
from django.test.runner import DiscoverRunner class NoMigrationTestRunner(DiscoverRunner): """ A test runner to test without database creation """ def setup_databases(self, **kwargs): """ Override the database creation defined in parent class """ pass def teardown_databases(self, old_config, **kwargs): """ Override the database teardown defined in parent class """ pass
The NoMigrationTestRunner extends the DiscoverRunner and directly overwrites the setup_databases and teardown_databases methods. These methods are used to setup certain connections, create database tables, and perform cleanup.
In this example, the setup and teardown methods are left blank to avoid creating new tables as well as to allow for the smooth usage of multiple databases.
Django is informed of the new test runner through a variable in the relevant settings configuration:
TEST_RUNNER = 'path_to.NoMigrationTestRunner'
Tests are executed using manage.py:
python manage.py test python<version> manage.py path.to.tests.TestModule --settings=<settings_file>
The first example is generic and collects tests found through our DiscoverRunner. The second command runs tests using a specific python version and a path to the relevant module which is imported using this string. A settings file was also provided in the second command.
Django has extensive documentation. However, as the framework changes, certain aspects will break. This article covered how to setup tests for multiple databases, not migrate databases in test, and how to execute our tests.