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/]