Testing in Django
Tags: testing, django, tutorial, python
Published: 2023-06-09 18:57:25

Testing is crucial in the software development lifecycle, ensuring that applications meet the required quality standards and function as expected. It systematically evaluates software components, modules, or systems to identify errors, bugs, or deviations from the desired behaviour.

Testing helps mitigate risk, enhance reliability, and improve the overall user experience.

The primary goal of testing is to uncover defects or discrepancies between the expected and actual results of a software system. 

By executing a series of predefined test cases, developers and quality assurance professionals can verify the correctness, completeness, and robustness of the software. Testing also helps identify performance bottlenecks, security vulnerabilities, and compatibility issues.

## Types of Tests

There are various types of tests that are commonly performed during the software development process, including:


- **Unit tests:** These tests focus on testing individual units or components in isolation.
- **Integration tests:** These tests verify the interaction and integration between multiple units.
- **System tests:** These tests verify the overall functionality of the software system.
- **Acceptance tests:** These tests are performed by users or customers to verify that the software meets their requirements.

Testing can be performed manually or using automated testing frameworks and tools. Automated testing is preferred in modern software development due to its efficiency, repeatability, and scalability. 

It allows for the creation of test suites that can be executed automatically, enabling developers to catch issues early and streamline the development process.

Testing is not a standalone activity in software development but rather an integral part of the development workflow. It should be performed continuously throughout the development cycle, starting from the early stages of requirements gathering and design, through implementation, and up to deployment and maintenance. 

This iterative testing approach ensures that defects are detected and fixed promptly, reducing the cost and effort associated with rework and bug fixing.

In this article, we will be looking at testing with Django. Let’s uncover what Django is.

## What is Django?

[Django](https://www.djangoproject.com/) is an open-source web framework in Python that follows the model–views–template (MVT) architectural pattern. Django provides a comprehensive set of tools, libraries, and features that enable developers to create scalable, secure, and maintainable web applications.

It is maintained by the [Django Software Foundation](https://www.djangoproject.com/foundation/). Some of the essential features in Django are a Ready-to-use admin interface/panel, support for Internationalization and Localization, Template Engine, Object-Relational Mapping (ORM), and Testing framework, among others.

Developers widely use Django due to its versatility, extensibility, and strong community support. It is a popular choice for web development in Python.

## Prerequisites

To follow through in this article, the following is needed

- Knowledge of Python and Django syntax.
- A bit of knowledge of testing.

## Testing with Django

To test in Django, the library has to first be downloaded. To download Django, we use `pip`. In this article, we will use [pip](https://pypi.org/project/pip/) to manage our downloads on our local computer. To do this, we need to run the command below in our terminal:

``` python
pip install django
```

After installing Django, we create a project with the `django-admin` keyword

``` python
django-admin startproject progy .
```

This creates the base boilerplate for our project. We then create an app. An app is a self-contained module or component that encapsulates a specific functionality or feature of a web application. We will call our app `account`.

``` python
django-admin startapp account
```

It would look similar to this after the creation.


![Screenshot of the Django app in VScode](https://paper-attachments.dropboxusercontent.com/s_819ECD89DABBDB96A16F567A63D41B69235230AB7C806CAAA137F5F0E25E5B6A_1685053588745_image.png)


Next, we include our app into the `INSTALLED APPS` in the `[settings.py]`

``` python
    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
    
        "account", # our app
    ]
```

And in our base `urls.py` , we include the app’s url file.
**Note:** Create a `urls.py` file to avoid errors.
``` python
from django.contrib import admin
from django.urls import path, include
    
url_patterns = [
    path('admin/', admin.site.urls),
    path('', include('account.urls')),
]
```

## Writing Unit Tests

Writing unit tests is a fundamental aspect of software development, and Django provides a comprehensive testing framework to facilitate the process. This is the most common form of test in Django. We will be looking at testing the views.
Our `views.py` code is as follows:

``` python
from django.http import HttpResponse
    
    def hello_world(request):
        """A simple view that returns a string "Hello, world!""""
        return HttpResponse("Hello, world!")
    
    def add_numbers(request):
        """A view that returns the sum of two numbers."""
        if request.method == 'POST':
            num1 = int(request.POST.get('num1'))
            num2 = int(request.POST.get('num2'))
            sum = num1 + num2
            return HttpResponse(f"The sum of {num1} and {num2} is {sum}.")
        else:
            return HttpResponse("Please submit the form to add two numbers.")

```
In Django, unit tests are typically organized into test case classes. We create a new class that inherits from Django's `django.test.TestCase`  in our `tests.py`. The unit cases will be based on the two functions we have above, `hello_world` and `add_numbers` respectively.
Before that, let us update our app’s `urls.py`

``` python
from django.urls import path
from . import views
    
app_name = 'accounts'
    
urlpatterns = [
    path('hello/', views.hello_world, name='hello_world'),
    path('add/', views.add_numbers, name='add_numbers'),
]
```

The unit tests for these functions will look like:

``` python
from django.test import TestCase
    from django.urls import reverse
    
    class HelloWorldViewTest(TestCase):
        def test_hello_world(self):
            url = reverse('accounts:hello_world')
            response = self.client.get(url)
            self.assertEqual(response.status_code, 200)
            self.assertEqual(response.content.decode(), "Hello, world!")
    
    class AddNumbersViewTest(TestCase):
        def test_add_numbers_get(self):
            url = reverse('accounts:add_numbers')
            response = self.client.get(url)
            self.assertEqual(response.status_code, 200)
            self.assertContains(response, "Please submit the form to add two numbers.")
    
        def test_add_numbers_post(self):
            url = reverse('accounts:add_numbers')
            data = {'num1': '5', 'num2': '3'}
            response = self.client.post(url, data)
            self.assertEqual(response.status_code, 200)
            self.assertContains(response, "The sum of 5 and 3 is 8.")
```

To ensure our tests run and to see the result, we use the command `python manage.py test` and this gives us:

![Screenshot of the test being run.](https://paper-attachments.dropboxusercontent.com/s_819ECD89DABBDB96A16F567A63D41B69235230AB7C806CAAA137F5F0E25E5B6A_1685053626265_image.png)

This process creates a test database and destroys it afterward.

## Writing Integration Tests

In the context of web development with Django, integration tests involve testing the collaboration between various parts of the application, such as views, models, templates, and the database.

For our integration test, we would be creating a model in our `models.py` for testing.

``` python
 from django.db import models
    
    class Name(models.Model):
        """A name model."""
        name = models.CharField(max_length=100)
        description = models.TextField()
    
        def __str__(self):
            return self.name
```

And our `views.py` will return the list of names we have in our database

``` python
def name_list(request):
    """A view that returns a list of names."""
    names = Name.objects.all()
    return render(request, 'account/name_list.html', {'names': names})
```

The `urls.py`

``` python
from django.urls import path
from . import views
    
app_name = 'accounts'
    
urlpatterns = [
    path('names/', views.name_list, name='name_list'),
]

```
And our integration test in the `tests.py` will be:
   
``` python
from django.test import TestCase, Client
    from django.urls import reverse
    from .models import Name
    
    class NameIntegrationTest(TestCase):
        def setUp(self):
            self.client = Client()
            self.url = reverse('accounts:name_list')
    
        def test_name_list_integration(self):
            # Create test data
            Name.objects.create(name='John', description='John Doe')
            Name.objects.create(name='Jane', description='Jane Smith')
    
            # Send a GET request to the URL
            response = self.client.get(self.url)
    
            # Assert the response status code and content
            self.assertEqual(response.status_code, 200)
            self.assertContains(response, 'John')
            self.assertContains(response, 'Jane')

```
It's important to note that integration tests complement unit tests, which focus on testing individual units or components in isolation. Both types of tests are valuable for ensuring the overall quality and reliability of a software application.

## Writing Functional Tests

Functional tests, also known as end-to-end tests or acceptance tests, are software testing that verifies the functionality of an application from the user's perspective. In Django, functional tests simulate user interactions with the application and validate the expected behaviour. Using our existing example from the `models.py` , `views.py`, and `urls.py` , we create a functional test in our `tests.py` with Selenium in this article.

[Selenium](https://www.selenium.dev/) is an open-source framework commonly used for automating web browsers. It provides a set of tools and libraries for interacting with web browsers and automating browser actions, such as clicking buttons, filling out forms, and navigating through web pages. It also supports many languages and [Python](https://selenium-python.readthedocs.io/) is one of them.
Our `tests.py` becomes

``` python
from django.test import LiveServerTestCase
from selenium import webdriver
from .models import Name
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By

class NameFunctionalTest(LiveServerTestCase):
    def setUp(self):
        self.selenium = webdriver.Chrome()
        super().setUp()

    def tearDown(self):
        self.selenium.quit()
        super().tearDown()

    def test_name_list_functional(self):
        # Create test data
        Name.objects.create(name='John', description='John Doe')
        Name.objects.create(name='Jane', description='Jane Smith')

        # Simulate user interactions using Selenium
        self.selenium.get(self.live_server_url + '/names/')
        self.assertIn('Name List', self.selenium.title)
        names =self.selenium.find_elements(By.TAG_NAME, 'li')
        self.assertEqual(len(names), 2)
        self.assertEqual(names[0].text, 'John - John Doe')
        self.assertEqual(names[1].text, 'Jane - Jane Smith')

        # Simulate the page returns a 200
        self.assertEqual(self.selenium.title, 'Name List')
```
After the test command is being run, a Chrome browser will quickly pop up and perform the test.

## Test Coverage and Reporting

Test coverage is a measure of the extent to which our code is being tested by our test suite. Test coverage is essential for ensuring the quality and reliability of our software.

Django has various tools for measuring test coverage and generating reports. One popular tool is `coverage.py`, a Python library that tracks which parts of the code are executed during the test run.

To use `coverage.py` for test coverage and reporting in Django, let us go through the following steps

**1.** Install coverage with pip by running the following command

``` python
pip install coverage
```

**2.** Run tests with coverage by using the `coverage` command

``` python
coverage run manage.py test
```
 
**3.** After running the tests, we can generate the coverage report using the `coverage report` command. This command shows the coverage summary in the terminal:


``` python
coverage report
coverage html # to generate an HTML report that is well detailed
```

To customize the coverage, a `.coveragerc` configuration file can be created at the root directory of our Django project. For example, the coverage of my test for my code was:

![Screenshot of the .coveragc directory](https://paper-attachments.dropboxusercontent.com/s_819ECD89DABBDB96A16F567A63D41B69235230AB7C806CAAA137F5F0E25E5B6A_1685053647814_image.png)

## Test Fixtures and Mocking

Test fixtures and mocking are important techniques in software testing that help create controlled environments and isolate dependencies to ensure reliable and predictable test results.

### Text Fixtures
Test fixtures are a set of predefined data, configuration, or state that is set up before running tests. They help create a consistent and known starting point for tests, allowing us to reproduce specific conditions and behaviours. In Django, we can use fixtures to load data into our test database.

To create a fixture file, we first create a JSON file that defines the test data we want to use. For example, let’s create a `sample.json` file with the following entries:

``` json
[
  {
    "name": "Herlet Skim",
    "description": "A random person that exists out of nowhere but has a pretty interesting life."
  },
  {
    "name": "John Doe",
    "description": "A fictional character who is often used as a placeholder name."
  },
  {
    "name": "Jane Doe",
    "description": "A fictional character who is often used as a placeholder name for a woman."
  },
  {
    "name": "Sherlock Holmes",
    "description": "A fictional detective created by Sir Arthur Conan Doyle."
  },
  {
    "name": "Dr. Watson",
    "description": "A fictional doctor and the partner of Sherlock Holmes."
  }
]
```

After having a `sample.json` file, we can load the fixtures using the `fixtures`  attribute of the test case. By specifying the fixture file in the `fixtures` attribute, Django will load the test data defined in the fixture before running the test method.

``` python
from django.test import TestCase

class MyTestCase(TestCase):
    fixtures = ['sample.json']

    def test_something(self):
        # Test logic here 
```

### Mocking
Mocking, on the other hand, is a process of replacing real objects or functions with test-specific versions that replicate their behaviour. In Python/Django, the `unittest.mock` module provides powerful mocking capabilities.

To get started with mocking, we import the `mock` module

```
from unittest.mock import Mock
```

Using our previous data from the `models.py` , `views.py`, and `urls.py` , we create a new `tests.py` for mocking


``` python
from django.test import TestCase, RequestFactory
from unittest.mock import Mock, patch
from .models import Name
from .views import name_list

class NameListViewTest(TestCase):
    def setUp(self):
        self.factory = RequestFactory()

    def test_name_list_view(self):
        # Create some sample Name objects
        Name.objects.create(name='John Doe', description='Description 1')
        Name.objects.create(name='Jane Smith', description='Description 2')

        # Create a mock request object
        request = self.factory.get('/names/')

        # Create a mock queryset for the Name objects
        mock_queryset = Mock(spec=Name.objects.all())
        mock_queryset.return_value = [
            Mock(name='John Doe', description='Description 1'),
            Mock(name='Jane Smith', description='Description 2')
        ]

        # Patch the Name.objects.all() method to return the mock queryset
        with patch('account.views.Name.objects.all', mock_queryset):
            # Call the name_list view
            response = name_list(request)

        # Assert that the response has the expected status code
        self.assertEqual(response.status_code, 200)

        # Assert that the response contains the expected data
        self.assertContains(response, 'John Doe')
        self.assertContains(response, 'Jane Smith')

```

## Testing Best Practices

When it comes to testing, following best practices can significantly improve the effectiveness and efficiency of our testing process. Some of the best testing practices to consider are:


**1. Start with a testing strategy:** Define a clear testing strategy that outlines the goals, objectives, and approach for testing software. Identify the types of tests to perform (unit tests, integration tests, etc.), determine the scope of testing, and establish guidelines for test coverage.

**2. Write testable code:** Design code to make it easy to test. Follow principles like SOLID and DRY to write modular, decoupled, and reusable code. Separate business logic from presentation and external dependencies, which allows for easier unit testing.

**3. Follow the AAA pattern:** When writing unit tests, structure them using the Arrange-Act-Assert (AAA) pattern. Arrange the test data, set up the necessary preconditions, act on the tested code, and assert the expected results or behaviours.

**4. Test edge cases and boundary conditions:** Ensure that the tests cover a wide range of scenarios, including edge cases and boundary conditions. Test inputs at their minimum and maximum values, empty or null values, and any special cases that could affect the behaviour of the code.

**5. Monitor code coverage:** Measure and monitor code coverage to ensure that tests adequately cover the codebase. Aim for high code coverage to increase confidence in the reliability and correctness of the software.

**6. Maintain and update tests:** Keep tests up to date with changes in the codebase. As changes are made to the application, review and update tests accordingly. Outdated tests can give false positives or false negatives, leading to unreliable results.

## Test Automation and Continuous Integration

Test automation and continuous integration are key practices in software development that help improve efficiency, quality, and reliability. Let's explore these two concepts in more detail.

### Test Automation
This involves using tools and scripts to automate the execution of tests, reducing manual effort and enabling faster and more frequent testing. It involves writing code to automate the setup, execution, and validation of test cases. Some of the benefits of test automation include:

**1. Improved Efficiency:** Automated tests can be executed much faster compared to manual tests, enabling faster feedback on the quality of the software.

**2. Increased Test Coverage:** With test automation, it becomes easier to achieve high test coverage by running a larger number of tests within a shorter timeframe.

**3. Integration with CI/CD:** Automated tests can be seamlessly integrated into the CI/CD pipeline, enabling continuous testing and faster delivery of software.

**4. Consistency:** Automated tests ensure consistent test execution and eliminate human errors that may occur during manual testing.

Popular test automation frameworks and tools for Django include [unittest](https://docs.python.org/3/library/unittest.html), [pytest](https://docs.pytest.org/en/7.3.x/), [Selenium](https://selenium-python.readthedocs.io/), [WebDriver](https://www.selenium.dev/documentation/webdriver/) for browser automation, and Django's built-in test framework.

### Continuous Integration (CI)
Continuous Integration is a development practice where developers frequently merge their code changes into a shared repository. The CI process involves automatically building and testing the software with each code commit. Some of the key aspects of CI are:

**1. Automated Builds:** CI systems automatically build the software upon each code commit, ensuring that the codebase remains in a consistent and functional state.

**2. Automated Testing:** Automated tests, including unit tests, integration tests, and even UI tests, are executed as part of the CI process. This ensures that code changes do not introduce regressions or break existing functionality.
**3. Integration with Version Control:** CI systems are integrated with version control systems like Git, allowing them to monitor code changes and trigger the build and test process automatically.
**4. Deployment Readiness:** CI helps ensure that the software is always in a deployable state. If the build and tests pass successfully, the software is ready for deployment.

Popular CI tools include Jenkins, Travis CI, CircleCI, and GitLab CI/CD.

By combining test automation and continuous integration, we can establish a robust and efficient software development process. Automated tests also provide quick feedback on code changes, detect issues early, and improve overall software quality.

## Conclusion

Testing is an important aspect of the software development cycle as it helps for quality code, reliability, and correctness.

By adopting a comprehensive testing approach in our Django projects, we can improve our applications' stability, maintainability, and overall quality.

It helps developers deliver products that meet user expectations, comply with specifications, and withstand real-world usage scenarios.

By incorporating testing practices into their development workflows, software teams can achieve higher quality assurance and customer satisfaction levels.