Unit tests in Django

Manual testing of websites becomes more difficult as they expand in size. Not only is there more to test, but as component relationships become more complicated, a slight change in one area might influence other areas. The latter will necessitate more testing to guarantee everything continues to operate and no mistakes are created as more changes are made.

Writing automated tests, which can be and reliably executed every time you make a change, is one method to alleviate these issues. This tutorial demonstrates how to use Django’s test framework to automate unit testing of your website.

In the software development process, testing is a crucial phase. Many software developers skip this stage and manually test their code.

As the scale of the program grows, manual code testing becomes onerous. Unit tests verify that every component you add to your project functions as intended without interfering with other features’ operation.

Because a website is built up of numerous logic layers, such as managing HTTP requests, form validation, and rendering templates, testing it can be difficult. On the other hand, Django comes with a set of tools that make testing your web application a breeze. The Python unittest module is the preferred way to write tests in Django, while other testing frameworks can be used.

This tutorial will walk you through a basic Django application that allows school administrators to store information about admitted pupils. We’ll write unit tests for each of our application’s components.

Testing Methodologies

Tests and testing procedures come in a variety of sorts, degrees, and categories. The following are the essential automated tests:

Unit tests

It is practically hard to create websites that perform the first time flawlessly. As a result, you must test your web application to identify these mistakes and address them as soon as possible. It is customary to divide testing into units that test specific web application capabilities to improve test efficiency. Unit testing is the term for this procedure. Because the tests focus on specific pieces (units) of your project independent of other components, making it easier to spot mistakes.

Unit tests, in summary, are a type of test used to check the functionality of individual components, usually at the class and function level.

Regression Tests

These are tests that re-create bugs from the past. Each test is performed once to ensure that the bug has been repaired. It also guarantees that it has not been re-created due to further code modifications.

Integration Tests

Examine how component groupings interact when used together. Integration tests are aware of required component interactions but not necessarily of each component’s underlying activities.

Creating the Application

First, we will create a directory called django_unit_tests as follows

mkdir django_unit_tests && django_unit_tests

Then we will create a virtual environment as follows.

virtualenv unittests_env

Subsequently, we will activate the virtual environment by running the following command.

source unittests_env/bin/activate

At this stage, we can now install Django using pip as follows.

pip install django==3.2.6

Now, we can create a Django application by running the following command.

django-admin startproject djangotesting

It will create a Django testing application

The most common way to organize a Django project is into applications. Larger projects are easier to handle as a result of this. Let’s start by running the command below to build a new Django application in our project.

First, we will change the directory into the testing application using the following command.

cd djangotesting

Because that is where the manage.py file is located.

$ python manage.py startapp employee_testing

By using the command below, you may add Django restframework to your application.

pip install djangorestframework

Once djangorestframework is installed, we need to add it to the INSTALLED_APPS list in the settings.py file, as seen below.

# settings.py

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'testing.apps.TestingConfig',
'rest_framework'
]

Models

In the models.py file, we will add the following code snippet.

# models.py

class Employee(models.Model):
  
  first_name = models.CharField(max_length=50)
  last_name = models.CharField(max_length=50)
  reg_number = models.CharField(max_length=50)
  date_of_admission = models.DateField(null=True, blank=True)

  def get_absolute_url(self):
    return reverse("employee-detail", args=[str(self.id)])

  def __str__(self):
    return f"Name: {self.first_name} {self.last_name}"

An Employee model was generated in the database as an Employee table in the code excerpt above. There are two methods in the model:-

get_absolute_url(self): returns the URL to a particular employee detail page.

str(self): returns a string representation of the employee’s first and last names.

Serializer

Would you please create a new Python file called serializers.py in the testing application and paste the code below into it?

# serializers.py

class EmployeeSerializer(ModelSerializer):
  class Meta:
    model = Employee
    fields = "all"

The preceding code line changes the Employee model to JSON and back. JSON data is simple to send over HTTP. As a result, the data is transformed to JSON format.

Add the code snippet below to the views.py file.

class EmployeeListView(generic.ListView):
  model = Employee
  paginate_by = 10 # the number of employees to return in each page
  
class EmployeeView(generic.DetailView):
  model = Employee

There are two view classes in the code excerpt above:-

The EmployeeListView returns a list of employees.

EmployeeView is a view that provides detailed information about an employee.

View API

In the testing application, create a new python file called api_views.py and add the code snippets below.

# api_view.py

class CreateEmployeeApiView(generics.CreateAPIView):
  queryset = Employee.objects.all()
  serializer_class = EmployeeSerializer

A class is included in the preceding code snippets that allow employees to create a REST API.

URLs

Create a new urls.py file in the testing application and add the code snippets below.

#employee_testing/urls.py

urlpatterns = [
 path('employees', EmployeeListView.as_view(), name="employees"),
 path('employee/create', CreateEmployeeApiView.as_view(), name="create-employee"),
 path('employee/<int:id>', EmployeeView.as_view(), name="employee-detail")
]

The paths to various views in the program are contained in the code snippets above.

As illustrated below, edit the testing application URLs in the root project’s urls.py file.

# djangotesting/urls.py

urlpatterns = [
path('admin/', admin.site.urls),
path('', include('testing.urls'))
]

Template

Make a new directory called templates in the project directory. Then, create a new directory called testing in the templates directory you just made. This folder will house the testing application’s template files.

Add the code snippet below to a new HTML file named employee_list.html in the testing directory of the templates directory.

<!-- testing/employee_list.html -->
<!doctype html>
<html lang="en">

<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
        integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
    <title>Employee enrollment application</title>
</head>

<body>
    <div class="container" style="margin-top: 100px;">
        <table class="table">
            <thead class="thead-dark">
                <tr>
                    <th scope="col">#</th>
                    <th scope="col">Employee First Name </th>
                    <th scope="col">Employee Last Name</th>
                    <th scope="col">Employee Reg. No.</th>
                </tr>
            </thead>
            <tbody>
                <!-- prints out the employees details in a table -->
                {%% for employee in employee_list %%}
                <tr>
                    <th scope="row">{{ employee.id }}</th>
                    <td>{{ employee.first_name }}</td>
                    <td>{{ employee.last_name }}</td>
                    <td>{{ employee.reg_number }}</td>
                </tr>
                {%% endfor %%}
            </tbody>
        </table>
    </div>


    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
        integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
        crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
        integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
        crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
        integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
        crossorigin="anonymous"></script>
</body>

</html>

Execute the instructions below to test the program and confirm everything is working correctly.

$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py runserver

Unit Tests: How to Write Them

We’ll begin by writing tests for our views. Create a new python package called tests in the testing application.

How to test views

Create a new python file called tests_views.py in the tests package you just made.

Note: It is standard practice for test files, to begin with, the word tests.

Add the following code to the tests_views.py file you created before.

# test_views.py

class EmployeeListViewTest(TestCase):
  
	@classmethod
	def setUpTestData(cls):
      number_of_employees = 30
      for employee_id in range(number_of_employees):
        Employee.objects.create(first_name=f"John{employee_id}", last_name=f"Doe{employee_id}")
        
    def test_url_exists(self):
      response = self.client.get("/employees")
      self.assertEqual(response.status_code, 200)

    def test_url_accessible_by_name(self):
        response = self.client.get(reverse('employees'))
        self.assertEqual(response.status_code, 200)

    def test_view_uses_correct_template(self):
        response = self.client.get(reverse('employees'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'testing/employee_list.html')

    def test_pagination_is_correct(self):
        response = self.client.get(reverse('employees'))
        self.assertEqual(response.status_code, 200)
        self.assertTrue('is_paginated' in response.context)
        self.assertTrue(response.context['is_paginated'] is True)
        self.assertEqual(len(response.context['employee_list']), 10)

The method setUpTestData(cls) is marked @classmethod since it is called first when the class is run. We create employee objects in this function, save them in a temporary test database, and use them throughout the test class.

The test_url_exists method initiates an HTTP call to the specified path and checks for a successful response code.

test_url_accessible_by_name techniques are used to see if a URL is available by name. First, create a URL from the specified name, send an HTTP request, and examine the response’s status code.

test_view_uses_correct_template – When the specified path is visited, this method affirms if the correct template is loaded.

The method test_pagination_is_correct checks if the data returned is paginated. Otherwise, this test will fail.

Testing of Models

Would you please create a new file named test_models.py in the test package and paste the code snippets below into it?

# test_models.py

class EmployeeModelTestcase(TestCase):
  @classmethod
  def setUpTestData(cls):
    Employee.objects.create(first_name="Peter", last_name="John", reg_number="111b2")
    
  def test_string_method(self):
    employee = Employee.objects.get(id=1)
    expected_string = f"Name: {employee .first_name} {employee .last_name}"
    self.assertEqual(str(employee), expected_string)

  def test_get_absolute_url(self):
      employee = Employee.objects.get(id=1)
      self.assertEqual(employee.get_absolute_url(), "/employees/1")

In the test code above:-

The setUpTestData function sets up the object that will be utilized throughout the test class.

The test_string_method method checks whether the string returned by the Employee model’s str method is legitimate.

The test_get_absolute_url method verifies that the model’s absolute URL is correct.

Testing Views from APIs

Would you please create a new Python file called tests_api_view.py in the tests package and paste the code snippets below into it?

# test_api_view.py

class EmployeeSerializerTestCase(APITestCase):
  
  def employee_creation_test(self):
    payload = {
    "first_name": "Tom",
    "last_name": "Ann",
    "reg_number": "Bob",
    "reg_date": datetime.date.today()
    }
	response = self.client.post(reverse("employee-create"), payload)
	self.assertEqual(status.HTTP_201_CREATED, response.status_code)

There is only one method in the code excerpt above:-

To test the employee creation endpoint, use employee_creation_test, we make a POST request to the employees/create an endpoint with the payload and then generate a payload containing all of the data required to create an employee.

We used APITestCase from restframework instead of TestCase from Django in the test_api_view.

Run the command below in the terminal in the current working directory to run our tests.

$ python manage.py test

Complete Source Code for reference

Below are code snippets for different sections for your reference to compare in case you missed out on any of the sections while reading through the article.

settings

# djangotesting/settings.py

"""
Django settings for djangotesting project.

Generated by 'django-admin startproject' using Django 3.2.6.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-x6si2ioub15=a5+d5v_7!u-xh4+rs(aw#rq1fuut*vho534(^v'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'employee_testing',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'djangotesting.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'djangotesting.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

employee_testing

# djangotesting/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
 path('admin/', admin.site.urls),
 path('', include('employee_testing.urls'))
]
# employee_testing/views.py

from django.views import generic

from .models import Employee


class EmployeeListView(generic.ListView):
    model = Employee
    paginate_by = 10


class EmployeeView(generic.DetailView):
    model = Employee

# employee_testing/urls.py

from django.urls import path

from .api_view import CreateEmployeeApiView
from .views import EmployeeListView, EmployeeView

urlpatterns = [
    path('employee', EmployeeListView.as_view(), name="employees"),
    path('employee/create', CreateEmployeeApiView.as_view(), name="create-employee"),
    path('employee/<int:id>', EmployeeView.as_view(), name="employee-detail")
]

# employee_testing/serializer.py

from rest_framework.serializers import ModelSerializer

from .models import Employee


class EmployeeSerializer(ModelSerializer):
    class Meta:
        model = Employee
        fields = "__all__"

# employee_testing/models.py

from django.db import models
from django.urls import reverse


class Employee(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    reg_number = models.CharField(max_length=50)
    reg_date = models.DateField(null=True, blank=True)

    def get_absolute_url(self):
        return reverse("employee-detail", args=[str(self.id)])

    def __str__(self):
        return f"Name: {self.first_name} {self.last_name}"

# employee_testing/api_view.py

from rest_framework import generics

from .models import Employee
from .serializer import EmployeeSerializer


class CreateEmployeeApiView(generics.CreateAPIView):
    queryset = Employee.objects.all()
    serializer_class = EmployeeSerializer
    

tests

# tests/test_models.py

from django.test import TestCase

from djangotesting.employee_testing.models import Employee


class StudentModelTestcase(TestCase):
    @classmethod
    def setUpTestData(cls):
        Employee.objects.create(first_name="ann", last_name="brown", reg_number="4123")

    def test_string_method(self):
        employee = Employee.objects.get(id=1)
        expected_string = f"Name: {employee.first_name} {employee.last_name}"
        self.assertEqual(str(employee), expected_string)

    def test_get_absolute_url(self):
        employee = Employee.objects.get(id=1)
        self.assertEqual(employee.get_absolute_url(), "/employees/1")
# tests/tests_api_view.py

import datetime

from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase


class EmployeeSerializerTestCase(APITestCase):
    def student_creation_test(self):
        payload = {
            "first_name": "ann",
            "last_name": "brown",
            "reg_number": "4123",
            "reg_date": datetime.date.today()
        }
        response = self.client.post(reverse("employee-create"), payload)
        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
# tests/tests_views.py

from django.test import TestCase
from django.urls import reverse

from ..models import Employee


# test_views.py

class EmployeeListViewTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        number_of_students = 30
        for student_id in range(number_of_students):
            Employee.objects.create(first_name=f"John{student_id}", last_name=f"Doe{student_id}")

    def test_url_exists(self):
        response = self.client.get("/students")
        self.assertEqual(response.status_code, 200)

    def test_url_accessible_by_name(self):
        response = self.client.get(reverse('students'))
        self.assertEqual(response.status_code, 200)

    def test_view_uses_correct_template(self):
        response = self.client.get(reverse('students'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'testing/employee_list.html')

    def test_pagination_is_correct(self):
        response = self.client.get(reverse('students'))
        self.assertEqual(response.status_code, 200)
        self.assertTrue('is_paginated' in response.context)
        self.assertTrue(response.context['is_paginated'] is True)
        self.assertEqual(len(response.context['student_list']), 10)

Conclusion

Writing test code is not enjoyable, and as a result, it is frequently done last (if at all) when building a website. However, it is necessary to ensure that your code is safe to distribute when modifications are made and cost-effective to maintain.

We’ve covered how to write and run tests for your models, forms, and views in this article. But, most importantly, we’ve included a quick explanation of what you should test, which is frequently the most challenging part of the process when you’re just getting started.

There’s still a lot to learn, but you should be able to write successful unit tests for your websites using what you’ve already learned.

Create a RESTful endpoint for our employee application and add unit tests for the serializers and API views now that you know how to build unit tests for various components in a Django application.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *