Writing effective tests in Django & Django REST Framework
Tags: django, python, testing
Published: 2022-06-04 14:26:32

## Overview
Let’s explore ways to write effective tests in **Django** and **Django REST framework**.  We’ll cover the following topics in this post:
- Test factory and Faker
- Happy Paths, Exceptions, and Errors

## Test factory and Faker
When writing tests that require data, we usually create the model's data first and then pass them in assertions:

``` python
blog = Blog.objects.create(title='Django is awesome')
self.assertEqual(blog.title, 'Django is awesome')
```

While this is not a bad implementation, we should replace all declarations of `model.objects.create` using test factories. Why do this? One good answer is reusability. A **test factory** works the same way as a model but _without the need to manually create the data in tests_.

**models.py**
``` python
from django.db import models

class Blog(models.Model):
  content = models.TextField()
  summary = models.CharField(max_length=150)
  title = models.CharField(max_length=100)
```

**factories.py**
``` python
from factory import Faker
from factory.django import DjangoModelFactory

class BlogFactory(DjangoModelFactory):
  class Meta:
      model = 'app.Blog'
  
  content = Faker('sentence', nb_words=12)
  summary = Faker('sentence', nb_words=4)
  title = Faker('sentence', nb_words=2)
```

Here I'm using [factory_boy](https://factoryboy.readthedocs.io/en/stable/index.html) to create the factory class for `Blog`. This package contains a wrapper for [Faker](https://faker.readthedocs.io/en/master/), which we can use to create dummy data for our model fields. With this, test cases that require the use of `Blog` can now be written in a leaner fashion. Observe the example below:

**tests.py**
``` python
from app.factories import BlogFactory
from app.serializers import BlogSerializer

from rest_framework.test import APITestCase

class ReadBlogServiceTest(APITestCase):
  def setUp(self):
    # creates an accessible data in the service
    self.blog = BlogFactory()
    self.serializer = BlogSerializer(self.blog, many=False)
    self.response = self.client.get(f'/blogs/{self.blog.id}')

  def test_returns_blog(self):
    self.assertEqual(self.response.data, self.serializer.data)
```

The data is created behind the scene by `BlogFactory`. This includes the model's id, which is why I can access it via `self.blog.id`. If we need to declare the model field's value manually, then we need to pass it as parameter to the factory:

``` python
BlogFactory(title='This is a different title')
```

What about foreign keys? The same way as any model: create a factory for the child model. Then, instantiate it in the test case, and pass the id as parameter to the parent model's factory.

``` python
user = UserFactory()
BlogFactory(author=user.id)
```

Now, we can write solid, less verbose test cases for our projects.

## Happy Paths, Exceptions, and Errors
A **significant aspect** of testing is making sure that we capture the behaviors of what we are testing. Whether it is a granular part of the application or an entire workflow, it is important to _test the expected result for each scenario_. This is what I'm going to cover in this section.

Let's say we have an endpoint that retrieves a **student** based on its **id**. The id is passed as path parameter in the URL: `/students/<str:id>`, and is used to retrieve the associated record. The record is, then, serialized and returned in the response. Let's also add a rule that only students with the id between 1 and 50 can be retrieved, or else the endpoint will raise an exception.

``` python
from app.models import Student
from app.serializers import StudentSerializer

from rest_framework.exceptions import APIException
from rest_framework.views import APIView
from rest_framework.response import Response

class InvalidParameter(APIException):
    status_code = 406
    default_detail = 'The id passed is greater than 50.'
    default_code = 'invalid_parameter'

class ReadStudentService(APIView):
  def get(self, request, id):
    if id > 50:
      raise InvalidParameter

    student = Student.objects.get(pk=id)

    return Response(StudentSerializer(student, many=False).data)
```

Given these criteria the first thing we do is stub the data in our test case. We're going to use `StudentFactory` to create our student record:

``` python
from app.factories import StudentFactory, UserFactory
from app.serializers import StudentSerializer

from rest_framework.test import APITestCase

class ReadStudentServiceTest(APITestCase):
  def setUp(self):
    user = UserFactory()
    self.student = StudentFactory(author=user.id)
    self.serializer = StudentSerializer(self.student, many=False)
    self.response = self.client.get(f'/students/{self.student.id}')
```

Then, we proceed with happy path testing. What is a **happy path**? _It is a scenario in an application featuring no exceptions or errors_. In this example, the happy path is the endpoint returning the serialized student record. We can create three assertions here:

1. Assert that the http status is [200](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200)
2. Assert that the response returns a dictionary
3. Assert that the response returns the student record


``` python
def test_request_successful(self):
  # here I'm using status from rest_framework
  self.assertEqual(self.response.status_code, status.HTTP_200_OK)

def test_returns_dictionary(self):
  self.assertEqual(isinstance(self.response.data, dict), True)

def test_returns_student(self):
  self.assertEqual(self.response.data, self.serializer.data)
```

The advantage to writing all of these is that we are indirectly documenting the application's functionalities. From reading this test case alone, we know that when we pass a **valid id** in the student endpoint, _it should respond successfully and return a dictionary of the student record_. Note that we are also isolating each assertion in separate methods so that we can easily pinpoint failures when we run the test. This applies to exceptions and errors as well. 

The endpoint will raise `InvalidParameter` exception if the id passed is greater than 50. We can simulate this scenario by using `self.client.get` and supply the path parameter with the value 51 or higher. Then, we can use `assertRaises` and pass the exception class:

``` python
def test_invalid_param_raises_error(self):
  self.client.get(`/students/51`)
    
  # the exception is imported from the service file
  self.assertRaises(InvalidParameter)
```

Finally, let's identify the errors. Since we are testing an endpoint, we can think of the errors as http status codes for unsuccessful requests. We already have a scenario where the http status code is **406** if the endpoint raises `InvalidParameter` (_see the class in the service_). We can also add another scenario where the id passed does not match any student record. This will result to a **404** status code:

``` python
def test_request_with_invalid_params(self):
  response = self.client.get(`/students/51`)

  self.assertEqual(response.status_code, status.HTTP_406_NOT_ACCEPTABLE)

def test_request_not_found(self):
  response = self.client.get(`/students/10`)

  self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
```

And we have finally covered the behaviors of the student endpoint. 

## Conclusion
What I have discussed above is but a fraction of creating tests. There are other components that I did not include in this post such as serializers, authentication, middlewares, and so on. However, the methods that I provided can definitely be utilized when approaching those components. I hope this helped you in some way and also made you appreciate the importance of writing tests for your projects.

Happy coding!