Writing unit tests for a Django web application

As websites expand, manual testing becomes more challenging. Interactions between components can lead to errors in other areas, and more changes are needed to ensure everything continues to function correctly. To address these issues, automated tests can be used to run every time changes are made, ensuring reliable and consistent testing. This blogpost demonstrates how to automate unit testing of your website using Django’s test framework.

Types of tests

There are various levels or classes of testing that can be applied to a software project, with different targets–though they all have the same goal, which is to find bugs before the users do. Current best practice is to run tests as often as changes are made, so automation is crucial. Major classes of testing are:

  • Unit tests, which evaluate the functional behavior of individual components, down to the class and function level.

  • Regression tests, which replicate previous bugs and verify their resolution. These tests are executed again to ensure that the issue is not reintroduced.

  • Integration tests, which validate the interaction between groups of components. While they are aware of the required interactions between components, they may not be aware of each component’s internal operations. Integration tests can range from simple groupings of components to testing the entire website.

Writing tests

Django’s unit tests rely on the Python standard library module called unittest. This module uses a class-based approach to define tests.

An example of this approach is shown below. In Django, you can create a test case by subclassing django.test.TestCase, which is a subclass of unittest.TestCase. This ensures that each test is executed within a transaction, providing isolation between tests.

from django.test import TestCase

class MyTestCase(TestCase):
    def test_something(self):
        # Test code here

In this example, the MyTestCase class defines a test method called test_something The django.test.TestCase superclass provides additional testing tools and features beyond the standard unittest.TestCase class.

Another brief example would be:

from django.test import TestCase
from myapp.models import Animal


class AnimalTestCase(TestCase):
    def setUp(self):
        Animal.objects.create(name="dog", sound="bark")
        Animal.objects.create(name="cat", sound="meow")

    def test_animals_can_speak(self):
        """Animals that can speak are correctly identified"""
        dog = Animal.objects.get(name="dog")
        cat = Animal.objects.get(name="cat")
        self.assertEqual(dog.speak(), 'The dog says "bark"')
        self.assertEqual(cat.speak(), 'The cat says "meow"')

It’s essential to test all aspects of your code, but it’s not necessary to test libraries or functionalities provided as part of Python or Django.

Test Structure Overview

Django employs the test discovery feature of the unittest module, which automatically detects tests in any file with a name that follows the pattern of test*.py under the current working directory. As long as you name the files correctly, you can use any directory structure you prefer.

It’s advisable to organize your test code into a module and separate the files based on the types of code you want to test, such as models, views, forms, and others. Here’s an example structure:

myapp/
    __init__.py
    models/
        __init__.py
        test_models.py
    views/
        __init__.py
        test_views.py
    forms/
        __init__.py
        test_forms.py

In this example, we have a module called myapp that contains subdirectories for each type of code we want to test. Each of these subdirectories contains an __init__.py file and a separate test file named according to the pattern of test*.py. The __init__.py should be an empty file.

Typically, you can create a separate test class for each model, view, or form that you want to test, with individual methods for testing specific functionality. Alternatively, you may create a separate class to test a particular use case with individual test functions that validate each aspect of that use-case.

For instance, you could have a test class that validates the proper validation of a model field. This class could contain functions that test each possible failure case, ensuring that your code works correctly in every scenario. For example:

class TestClass(TestCase):

    def setUpTestData(cls):
        print("setUpTestData: Run once to set up non-modified data for all class methods.")
        pass

    def setUp(self):
        print("setUp: Run once for every test method to setup clean data.")
        pass

    def test_one_plus_one_equals_two(self):
        print("Method: test_one_plus_one_equals_two.")
        self.assertEqual(1 + 1, 2)

When writing test classes in Django, you can define two methods to configure the test environment:

  • setUpTestData(): This method is used for setting up objects that will not be modified by any test function.
  • setUp(): This method is used for setting up objects that may be modified by the test function. This ensures that each test function gets a fresh copy of the object for each test.

Test methods use Assert functions such as AssertTrue, AssertFalse, and AssertEqual to test conditions. If the condition doesn’t evaluate as expected, the test fails and reports the error to your console.

These Assert functions are standard assertions included in the unittest framework. Additionally, Django has specific assertions for testing if a view redirects (assertRedirects), if a specific template has been used (assertTemplateUsed), and others.

Running Tests

Tests can be run by executing the following command:

python3 manage.py test

When running tests using Django’s python3 manage.py test command, it will search for files that follow the pattern of test*.py in the current directory and run all tests that use the appropriate base classes. By default, the tests will only report on test failures and end with a summary of the tests executed.

Understanding the test output

When running tests in Django, the test runner will display various messages as it prepares itself.

Creating test database...
Creating table library
Creating table books

This tells you that the test runner is creating a test database, as described.

Once the test database has been created, Django will run the tests. If it goes well, you’ll see something like this:

----------------------------------------------------------------------
Ran 52 tests in 0.281s

OK

If there are test failures, however, you’ll see full details about which tests failed.

References

  • [1] [https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/Testing]
  • [2] [https://docs.djangoproject.com/en/4.2/topics/testing/overview/]