Running Django Tests Without Migrations

python_and_mouse

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.

Use Case

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.

Unittest Format

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.

Multiple Databases

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'

Execution

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.

Conclusion

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.

Advertisements