How to write tests in Django

By datnq, at: Jan. 31, 2024, 6:43 p.m.

Estimated Reading Time: 13 min read

How to use test in django.
How to use test in django.

How to write tests in Django

 

For most developers when starting out, writing tests might seem like a tedious task. However, in companies or universities, seasoned programmers, or instructors highly value writing tests and consider it a core part of application development. Understanding and mastering the art of writing tests helps improve performance, enhance application security, and elevate one's skillset. Specifically, today we are going to explore the Django unit tests.

 

1. Writing and Running Tests

 

Writing Tests

Django utilizes the standard unittest library for testing. Writing tests is straightforward. First, i create new simple model Animal.

from django.db import models

class Animal(models.Model):
      name = models.CharField()
      sound = models.CharField()

      def speak(self):
          return f'The {self.name} says "{self.sound}"'

 

Here, I'm creating a class named SimpleTest that inherits from Django's TestCase.

from django.test import TestCase
from myapp.models import Animal
 
class SimpleTestCase(TestCase):
    def setUp(self):
        Animal.objects.create(name="lion", sound="roar")
        Animal.objects.create(name="cat", sound="meow")
 
    def test_animals_can_speak(self):
        """Animals that can speak are correctly identified"""
        lion = Animal.objects.get(name="lion")
        cat = Animal.objects.get(name="cat")
        self.assertEqual(lion.speak(), 'The lion says "roar"')
        self.assertEqual(cat.speak(), 'The cat says "meow"')

 

The above test case consists of two main parts: setUp and test methods. setUp helps initialize common values for the test suite, and test methods use those common values to check the functionality of the application.

 

Running Tests

Once the tests are written, let's run the above test using the following command:

python manage.py test


When dealing with a large number of test cases, let's say an application with 2000-3000 test cases, and you only want to test a specific case or a specific set of tests, how would you do it? Fortunately, Django makes it easy.

# Run all the tests in the animals.tests module
$ python manage.py test animals.tests
 
# Run all the tests found within the 'animals' package
$ python manage.py test animals
 
# Run just one test case class
$ python manage.py test animals.tests.SimpleTestCase
 
# Run just one test method
$ python manage.py test animals.tests.SimpleTestCase.test_animals_can_speak

 

Output

After using the command to run tests, the application will create a mock database similar to the current database in use. All operations related to adding, editing, or deleting models will be performed on this mock database. We simply need to wait and see the results displayed in the command after running a series of test cases.

Found 3 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...
----------------------------------------------------------------------
Ran 3 tests in 0.073s
 
OK
Destroying test database for alias 'default'...

 

The above scenario is where all the tests have passed successfully. In case of failure in any test case, below is a part of the error message.

self.assertEqual(response.status_code, 201)
AssertionError: 200 != 201
 
----------------------------------------------------------------------
Ran 3 tests in 0.078s
 
FAILED (failures=1)
Destroying test database for alias 'default'...

 

2. Some Useful Libraries for Testing

 

Mock

Mock testing is a method in software testing where mock objects are used to replace real objects in the system. The goal of mock testing is to isolate a part of the system and test it independently of other components. Below is a simple example of using mock:

class MessageAPITestCase(APITestCase):
    @patch("api.views.MessageViewSet.list")
    def test_api_list_message(self, mock_list):
        mock_list.return_value = Response(
            {"field1""mocked_value""field2"99}
        )
        response = self.client.get("/api/messages/")
        mock_list.assert_called_once()
        self.assertEqual(response.status_code, 201)
        self.assertEqual(
            response.data, {"field1""mocked_value""field2"99}
        )

 

This code illustrates using mock to simulate an API. Instead of calling a third-party API and running the test, we assume that the API returns JSON values as expected. We then use that data to continue running the test. The diagram below helps understand mock testing better:

flow use mock

 

Factory Boy

As we know, initializing tests is always necessary. For small applications, initialization can be simple. However, when models have many relationships and are heavily interdependent, Factory Boy is a powerful tool to assist in initializing complex scenarios.

To initialize a model with Factory Boy, create a factory for that model:

import factory
from app.models import Message
from .user import UserFactory
from .room import RoomFactory
 
class MessageFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Message
 
    sender = factory.SubFactory(UserFactory)
    recipient = factory.SubFactory(UserFactory)
    room = factory.SubFactory(RoomFactory)
    content = "This is a message"

 

In the setUp method, instead of initializing using a queryset, we can create it like this:

self.room = RoomFactory.create()

 

Here, everyone is probably wondering, so which part of this code is fast? The model below will describe that:

 

process factory boy

 

BOOM! This makes it easy to create a model during testing without having to initialize a series of foreign keys.

 

3. URL Testing

In the modern era, URL testing is an essential part of web application development. Faced with various scenarios, from simple to complex, URL testing ensures that routes and views in your application work as expected.

from django.test import TestCase
from django.urls import reverse_lazy
 
class HomeTestCase(TestCase):
    def setUp(self):
        self.user1 = UserFactory.create()
 
    def test_get_success(self):
        url = reverse_lazy('my-view-name')
        response = self.client.get(url)
        self.assertEqual(200, response.status_code)

 

The above code is a simple example of URL Testing. Disregarding the simple setUp, focus on test_get_success. The URL is obtained from a hardcoded name through Django's reverse_lazy method, and the response includes everything the view returns.

 

4. Speeding Up Test Execution

 

a. Failfast

When running a test suite, you may not want to wait until all tests are completed to know the results. Failfast allows stopping the test run as soon as a test fails.

Pros: Stopping the test run upon encountering an error helps quickly identify and fix the issue without waiting for the entire test suite to complete. It also minimizes costs as you don't have to spend time and money when the application has a bug. Instead, you can focus on fixing the issue.

Cons: Sometimes failfast is not as omnipotent as we might think. For a programmer who wants to test all cases to gather statistics and understand where the errors are concentrated, failfast should not be used.

 

b. Keepdb

Usually, creating and dropping a database during testing can be time-consuming. Keepdb preserves the database after running tests, reducing the time needed to recreate the database for each test suite.

Pros: Speeds up continuous test execution: Keeping the database intact avoids the need to recreate data every time a test is run.

Cons: Data mixing risk: If a test changes data in the database, there is a risk of encountering issues with keeping the database.

 

c. Parallel

Dividing the test suite into parts and running them in parallel can make the best use of server resources and reduce the overall time for the entire test suite.

Pros: Reduces test execution time: Running tests in parallel makes efficient use of multiple CPUs.

Cons: Complicates data management: Ensuring that tests do not interact with the same data to avoid conflicts becomes necessary. Additionally, running tests in parallel shares initialization data between test cases, making it error-prone.

 

5. Directory Structure When Writing Tests

Let’s say we’ve just created a new Django app. The first thing we do is delete the default but useless tests.py module that python manage.py startapp creates. In its place, we create a directory called tests and place an empty init.py within. Inside that new directory, because most apps need them, we create test_forms.py, test_models.py, test_views.py modules. Tests that apply to forms go into test_forms.py, model tests go into test_models.py, and so on. Here’s what it looks like:


popsicles/
 __init__.py
 admin.py
 forms.py
 models.py
 tests/
  __init__.py
  test_forms.py
  test_models.py
  test_views.py
  views.py

 

Note that it is not mandatory for developers to write exactly as above, but it is advisable to do so. Adhering to common conventions makes the code easier to maintain and develop.


Related

Subscribe

Subscribe to our newsletter and never miss out lastest news.