Django Unit Test – Explained with Examples – Part 1

As the web application grows and more complex features are implemented, testing becomes inevitable. Testing can usually be done in many ways – the more the better. This includes manual tests performed by a human tester, unit tests, performance tests, integrations tests, etc.  In this article, we will focus merely on how to write and run a Django unit test.

If you wish to go straight to practical examples, skip to another post where I laid out an extensive set of Django unit test examples.

What are unit tests?

In programming, unit tests are a type of automated tests implemented by a programmer. Each unit test has the purpose of validating a behavior happening in a specific section (also called unit) or component of an app. Usually, unit tests are meant to only cover smaller chunks of code, leaving more complex scenarios to integration tests. These tests are meant to be run every time new changes are introduced, giving you clear signs if something broke in the process and needs to be fixed.

Writing unit tests guarantees fewer complications in the future when you’ll be shifting focus to other features. The more app’s behavior you test, the less you’ll have to worry when introducing new changes. 

All of this is well-known in the software development industry, where practices like Test-Driven Development (TDD) are widely adopted in agile teams. For example, TDD is a practice that focuses on writing unit tests even before implementing the actual code. This practice ensures that tests first fail, and then pass after writing the correct implementation.

Django unit test – How to write and run tests?

For testing purposes, Django comes with a test framework that is built on top of the unittest module from Python’s standard library. This module allows you to simulate and test all kinds of behaviors, such as receiving requests, validating permissions, submitting forms, querying the database, etc. 

NOTE - Django does not use your production database for testing. Also, you don’t need to manually create a test database for the purpose of testing. Django will automatically create a temporary database and name it by adding the test_ prefix to the NAME value from the DATABASES in settings.py.

Writing unit tests

To define a test case, create a file or set of files that will contain tests. Inside a test file, you can define multiple cases that need to be tested. Usually, each test case, which we can also call a Django unit test, contains multiple test methods that validate a specific behavior related to that case. To create an individual test case you have to derive a custom class from one of the Django test base classes (SimpleTestCase, TransactionTestCase, TestCase, LiveServerTestCase).

One of the most commonly used base classes for testing is the django.test.TestCase class. Test methods inside this class are run independently, but you can also define common behavior that should happen before and after running tests inside the setUpTestData, setUp and tearDown methods.

Validation inside the test methods is usually done with the help of Assert methods. These methods are used to compare expected and obtained results. 

Let’s take a look at the example of a simple test case defined inside the tests.py file.

from django.test import TestCase

class MyTestCase(TestCase):
    @classmethod
    def setUpTestData(cls):
        # This method is run only once, before any other test.
        # It's purpose is to set data needed on a class-level.
        print('setUpTestData')

    def setUp(self):
        # This method is run before each test.
        print('setUp')

    def tearDown(self):
        # This method is run after each test.
        print('tearDown')

    def test_my_first_method(self):
        # This method should perform a test.
        print('test_my_first_method')
        self.assertTrue(True)

    def test_my_second_method(self):
        # This method should perform a test.
        print('test_my_second_method')
        self.assertFalse(True)

In the above example, we can notice a few important things.

We created a MyTestCase class that was derived from Django’s TestCase class. Therefore, we got access to three additional methods:

  • setUpTestData method is used to define data and configuration that will be used throughout the entire testing process encapsulated inside this class. It is run only once before any other method.
  • setUp method is used for preparation before running each test method. Inside this method, you should define data that should be reset before every test.
  • tearDown method is similar to the setUp method but is used for cleaning up after each test.

Having that in mind, the order of execution in the above example would be:

  1. setUpTestData
  2. setUp
  3. test_my_first_method
  4. tearDown
  5. setUp
  6. test_my_second_method
  7. tearDown
NOTE - Each TestCase class starts with a clean database before running any tests and is torn down after completion. 

We also defined two custom test methods, test_my_first_method, and test_my_second_method that are using Assert methods to decide if the tests failed or passed. Let’s see how to run tests so we can verify the execution order.

Running unit tests

Django unit tests are executed with Debug=False, regardless of whether you run tests in development, staging, production, or any other environment. This way the app’s behavior is as closest as it can be to the behavior happening in production.

To execute the tests you wrote, run the test command:

$ python manage.py test

This will trigger discovery and execution of all test files inside a current project. Here I’m showing the output retrieved from the example we used in the previous section.

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
setUpTestData
setUp
test_my_first_method
tearDown
setUp
test_my_second_method
tearDown
======================================================================
FAIL: test_my_second_method (examplesapp.tests.test_examples.MyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\dummy\testproject\examplesapp\tests\test_examples.py", line 26, in test_my_second_method
    self.assertFalse(True)
AssertionError: True is not false

----------------------------------------------------------------------
Ran 2 tests in 0.022s

FAILED (failures=1)
Destroying test database for alias 'default'...

First, we can verify that the methods were executed in the previously mentioned order. Second, Django reported a failed test which is logical because when we inspect the test_my_second_method method, we can notice that the value True was passed to the assertFalse method. This triggered the AssertionError which caused this particular test to fail.

To fix this, we simply need to pass the value False to the assertFalse method.

def test_my_second_method(self):
    # This method should perform a test.
    print('test_my_second_method')
    self.assertFalse(True)

Now if we run the tests again, we can see that all tests passed and Django reported a success.

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
setUpTestData
setUp
test_my_first_method
tearDown
setUp
test_my_second_method
tearDown
----------------------------------------------------------------------
Ran 2 tests in 0.025s

OK
Destroying test database for alias 'default'...
WARNING - All test files should be starting with the test prefix (test*.py) so the Django’s test runner can discover them.

Sometimes you may want to target only particular tests. In that case, you need to specify the path as an additional parameter to the test command. 

To illustrate this, take a look at the following project structure. Notice that there’s a examplesapp with a tests package in it. This package has a file called test_examples.py that contains the MyTestCase we used as an example above.

testproject/
├─ examplesapp/
│  ├─ migrations/
│  │  ├─ 0001_initial.py
│  │  ├─ __init__.py
│  ├─ tests/
│  │  ├─ __init__.py
│  │  ├─ test_examples.py
│  ├─ admin.py
│  ├─ apps.py
│  ├─ models.py
│  ├─ views.py
│  ├─ __init__.py
├─ testproject/
│  ├─ asgi.py
│  ├─ settings.py
│  ├─ urls.py
│  ├─ wsgi.py
│  ├─ __init__.py
├─ manage.py

Here are examples of how to run the tests targeting different scopes.

# Run tests located in the examplesapp app
$ python manage.py test examplesapp.tests

# Run tests located in the test_examples.py file
$ python manage.py test examplesapp.tests.test_examples

# Run tests located in the MyTestCase class
$ python manage.py test examplesapp.tests.test_examples.MyTestCase

# Run tests located in the test_my_first_method method
$ python manage.py test examplesapp.tests.test_examples.MyTestCase.test_my_first_method

The previous example opens a question of how to organize tests in Django? Let’s address this question in the next section.

Unit tests coverage

One of the goals of writing tests is to validate as much as possible codebase in your project. But if the project grows larger every day, how to know exactly what is covered by tests? Don’t worry, there’s a useful tool that can help you with that. It’s called coverage.py and it provides a clear insight into what parts of your codebase are covered by tests. Let’s see how it works.

You first need to install the coverage.py package. You can do it via pip installer.

$ pip install coverage

To gather the data from the test, you need to run the test suite using the coverage run command. This will generate a file that coverage.py uses for creating a report.

$ coverage run manage.py test
NOTE - Think of the ‘coverage run’ command as a substitute for the ‘python’ command. You can use all additional parameters as you would usually.

To create a report from gathered data, run the coverage report command. This command will display the report in a terminal. An example of how the report could look like is shown below:

Name                                    Stmts   Miss  Cover
-----------------------------------------------------------
examplesapp\__init__.py                      0      0   100%
examplesapp\admin.py                         1      0   100%
examplesapp\apps.py                          4      0   100%
examplesapp\migrations\0001_initial.py       6      0   100%
examplesapp\migrations\__init__.py           0      0   100%
examplesapp\models.py                       19      5    74%
examplesapp\tests\__init__.py                0      0   100%
examplesapp\tests\test_examples.py          12      0   100%
examplesapp\urls.py                          2      0   100%
testproject\__init__.py                      0      0   100%
testproject\settings.py                     19      0   100%
testproject\urls.py                          3      0   100%
manage.py                                   12      2    83%
-----------------------------------------------------------
TOTAL                                       78      7    91%

Another way of displaying the report is by generating the HTML report. You can do that by running the coverage html command. This will create a htmlcov directory filled with data. Open the htmlcov/index.html in your browser to inspect the report. By clicking on a particular filename, another view will open showing you exact locations in your codebase that are not covered by tests.

How to organize unit tests in Django?

There are several ways to organize the structure of tests in a project and none is the best. This mostly depends on the type of project and file structure you are using. But to give you a sense of what it could look like, I’ll show you the two most common ways of organizing test files.

1. Define tests per logic layer

One way of structuring tests involves separating tests by logic layers. This means you would ideally need to create a tests package inside each app in your project, then put test files inside. Each test file would contain tests related to the logic layer being tested. 

So, for example, if your logic layers are divided by files like models.py, serializers.py, forms.py, views.py, etc. then you would need to create their test counterparts (test_models.py, test_serializers.py, test_forms.py, test_views.py). 

Below you can see an example of what this structure could look like.

libraryproject/
├─ bookrental/
│  ├─ migrations/
│  │  ├─ __init__.py
│  ├─ tests/
│  │  ├─ test_forms.py
│  │  ├─ test_models.py
│  │  ├─ test_serializers.py
│  │  ├─ test_views.py
│  ├─ admin.py
│  ├─ apps.py
│  ├─ forms.py
│  ├─ models.py
│  ├─ serializers.py
│  ├─ views.py
│  ├─ __init__.py
├─ libraryproject/
│  ├─ asgi.py
│  ├─ settings.py
│  ├─ urls.py
│  ├─ wsgi.py
│  ├─ __init__.py
├─ manage.py

2. Define tests per feature

Another way is to divide tests by features. Similar to the previous method, you would need to have a tests module inside each app in a Django project. Then you could divide tests by each feature implemented in the scope of a current app. 

For example, let’s say you have an app responsible for renting books, with features such as due date calculation and membership discount calculation. In such a case, your tests folder would contain test_due_date_feature.py and test_membership_discount_feature.py files responsible for testing mentioned features.

Below is an example of this structure.

libraryproject/
├─ bookrental/
│  ├─ migrations/
│  │  ├─ __init__.py
│  ├─ tests/
│  │  ├─ test_due_date_feature.py
│  │  ├─ test_membership_discount_feature.py
│  ├─ admin.py
│  ├─ apps.py
│  ├─ forms.py
│  ├─ models.py
│  ├─ serializers.py
│  ├─ views.py
│  ├─ __init__.py
├─ libraryproject/
│  ├─ asgi.py
│  ├─ settings.py
│  ├─ urls.py
│  ├─ wsgi.py
│  ├─ __init__.py
├─ manage.py

Best practices for writing a Django unit test

Programmers often take writing tests for granted, but writing tests really has a great return on investment. Because of that, it’s good to know some best practices. Below are some tips to help you ensure better test quality and therefore better code stability.

1. Don’t follow the DRY (Don’t-repeat-yourself) principle

Remember that purpose of a Django unit test is the validation of a smaller chunk of code. Think of each test as a separate unit of testing, independent of any other test method. While there’s some data you may want to share between tests (eg. by using the setUp method), do not try to follow the DRY (Don’t-repeat-yourself) principle. The goal is not to write the cleanest code inside a test method, but to validate a certain hypothesis.

2. Break a large test into multiple smaller ones

Although I mentioned not to follow all clean code practices, there’s one principle that actually should be followed. I’m talking about the SRP (Single-Responsibility Principle) that is perfectly applicable to test cases. If you are hesitant between writing multiple test hypotheses within one test method or separating into multiple test methods – always separate into multiple different test methods.

3. Test for failures

In your test methods you should test not only for success but for failures too. In other words, do not test only “happy” scenarios in which you preconfigured data and parameters for expected situations, but also write tests that will raise exceptions and return wrong results. That way, parts of the code you test will become bulletproof.

4. Write documentation for tests

The only indication of what the test is about is its name. To prevent confusion, my advice is to write documentation for all your tests. That way, you’ll be able to decode what’s happening inside failed tests much faster. The other reason for doing so is because maybe sometime in the future someone will inherit your code. 

5. Play a Test Coverage game

Using the coverage.py tool you can create a useful habit with a little game. It’s called Test Coverage Game. Let me explain the rules. As you already know, coverage.py reports what percentage of your code is covered with tests. The game rule is simple: every day that you increase your test coverage is a victory, and every day that the coverage goes down is a loss. That way, every time after writing a new piece of code, you’ll want to increase your test coverage; therefore you’ll be writing new tests.

6. Explore other great tools for testing

There are many great tools and libraries that can supplement your way of writing tests. After all, you don’t even have to use the unittest library to write tests if you feel more comfortable with some other library or tool. I will not be going into details, but here is a short resource list that can give you some ideas:

  • pytest-django – alternative to the unittest module.
  • factory_boy – used for generating model test data.
  • faker – used to generate fake data.
  • selenium – used for automating web applications.

Conclusion

This was a Django unit test overview – we answered multiple questions regarding unit tests. My final advice is to always use tests as an assurance of the quality of the code you are writing. Focusing on covering the critical parts of your application with tests will give you greater system stability when introducing new features.

After learning the theoretical background of Django unit tests, you are now ready to see some examples. Hop to my other post where I demonstrated how to apply this knowledge on an extensive set of examples.