What to test in Django: Endpoints
Tags: django, testing
Published: 2020-08-17 00:31:23

---
title: "What to test in Django: Endpoints"
published: true
description: "Testing makes a lot of sense. But what should you test? Let's take a look at testing endpoints in Django."
tags: [django, testing]
cover_image: "https://images.unsplash.com/photo-1532187863486-abf9dbad1b69?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80"
canonical_url: "https://alcher.dev/2020/what-to-test-in-django-endpoints/"
---

## Introduction

Times are changing, and data is king<sup><a href="#1">[1]</a></sup>. We usually store data within an RDBMS such as PostgreSQL or MySQL. Within a Django application, we manage and interface with our data using Django Models — something that we have explored [last time](https://alcher.dev/2020/what-to-test-in-django-models/). But this is not enough; we need a way to expose an interface in which this data is to be interacted with. Seeing that we are interested in building web applications, we are at mercy of the HTTP protocol. So we expose **API Endpoints** that allows clients to programatically interface with our data.

In this article, we will explore how we can test these API Endpoints.

## Endpoint Testing: Django REST Framework

Let's take a quick look at the [Django REST Framework](https://www.django-rest-framework.org/), which we'll refer as DRF from now on. As stated in their home page:

> Django REST framework is a powerful and flexible toolkit for building Web APIs.

And I can say with confidence, and probably thousands of other developers as well, that it indeed lives up to its tagline. It offers a LOT of functionality that makes API development easier, such as [Serializers](https://www.django-rest-framework.org/api-guide/serializers/) and [ViewSets](https://www.django-rest-framework.org/api-guide/viewsets/). The documentation is superb, the community support is great, and the mental model of how everything fits just makes sense.

As for testing, DRF provides the `APIClient` and `APITestCase` classes. These classes extend Django's `django.test.Client` and `django.test.TestCase`, respectively. Both of these components work with their Django counterparts, but the extra [goodies](https://www.django-rest-framework.org/api-guide/testing/#apiclient) do come handy from time to time.

Let's proceed by creating the endpoints that we'll be writing our tests for.

## Get them bread: DRF ViewSets and Serializers

Going against the spirit of [TDD](https://en.wikipedia.org/wiki/Test-driven_development), let's write a bunch of endpoints that will allow clients to do BREAD Operations<sup><a href="#2">[2]</a></sup> against a Django Model. We'll create a Film model to supplement the previous article:

```python
from django.db import models

class Film(models.Model):                             
    title = models.CharField(max_length=255)      
    description = models.CharField(max_length=255)
    release_year = models.DateField(auto_now=True)

    def __str__(self):                                 
      return self.title
```

We'll be using a [ModelSerializer](https://www.django-rest-framework.org/api-guide/serializers/#modelserializer) for the presentation logic:

```python
from restframework import serializers
from .models import Film

class FilmSerializer(serializers.ModelSerializer):
    class Meta:
        model = Film
        fields = "__all__"
```

... and a [ModelViewSet](https://www.django-rest-framework.org/api-guide/viewsets/#modelviewset) to implement the endpoints:


```python
from restframework import viewsets
from .serializers import FilmSerializer


class FilmViewSet(viewsets.ModelViewSet):
    queryset = Film.objects.all()
    serializer = FilmSerializer
```

All that is left to do is to register the viewset to a URL mapping within the `urls.py` file:

```python
from .views import FilmViewSet
from rest_framework.routers import DefaultRouter

app_name = "films"
router = DefaultRouter()
router.register(r'films', FilmViewSet)
urlpatterns = router.urls
```

With just 20 or so lines of code, we already have:

- Extensible data representation for both presentation and data access.
- Complete REST-ful routing, with sensible route names for URL lookup.

What would automated tests look like for these API endpoints?

### Test Browse and Read Endpoints

We'll be starting with the endpoints that fetches data for us:

```python
from rest_framework import status
from rest_framework.test import APITestCase
from .models import Film
from .serializers import FilmSerializer

class FilmViewsTest(APITestCase):
    @classmethod
    def setUpTestData(cls):
        # (1)
        cls.films = [Film.objects.create() for _ in range(3)]
        cls.film = cls.films[0]

    def test_can_browse_all_films(self):
        # (2)
        response = self.client.get(reverse("films:film-list"))

        # (3)
        self.assertEquals(status.HTTP_200_OK, response.status_code)
        self.assertEquals(len(self.films), len(response.data))

        for film in self.films:
            # (4)
            self.assertIn(
                FilmSerializer(instance=film).data,
                response.data
            )

    def test_can_read_a_specific_film(self):
        # (5)
        response = self.client.get(
            reverse("films:film-detail", args=[self.film.id])
        )

        self.assertEquals(status.HTTP_200_OK, response.status_code)
        self.assertEquals(
            FilmSerializer(instance=film).data,
            response.data
        )
```

This should hopefully all make sense as it's basically the same as testing a Django Model, or a Python class for that matter. The only difference is that instead of asserting the properties and methods of a class, we are interacting with its interface through HTTP. Let's take a closer look:

1. We create a set of data to test with.
2. We issue a `GET` request to the Film's "index" endpoint, which by convention will return all the available films in our system. Note that the reverse lookup name, aptly called `films:film-list`, is auto-generated by DRF's `ViewSet` class.
3. The `response` contains data related to, well, the response of our `GET` request. We assert that it did return a `200`, and the response body has the same number of items with the total films that we created.
4. Using the `FilmSerializer` class that we created earlier, we assert that each film in the response is properly formatted.
5. We do the same process for the test of reading a specific film, with the main difference that we are only checking a singular film instead of looping through a list of many.

### Test Add and Edit Endpoints

The Add, Edit, and Delete endpoints share an important characteristic: they all modify the database state in one way or another. We'll group the Add and Edit endpoints together because they function similarly enough:

```python
# ...


class FilmViewsTest(APITestCase):
    # ...
    
    
    def test_can_add_a_new_film(self):
        # (1)
        payload = {
            "title": "2001: A Space Odyssey",
            "description": 
                "The Discovery One and its revolutionary super " \
                "computer seek a mysterious monolith that first " \
                "appeared at the dawn of man.",
            "release_year": 1968
        }

        response = self.client.post(reverse("films:film-list"), payload)
        created_film = Film.objects.get(title=payload["title"])

        self.assertEquals(status.HTTP_201_CREATED, response.status_code)
        # (2)
        for k, v in payload.items():
            self.assertEquals(v, response.data[k])
            self.assertEquals(v, getattr(created_film, k))

    def test_can_edit_a_film(self):
        # (3)
        film_data = {
            "title": "InceptioN",
            "description": 
                "Cobb steals information from his targets by "  \
                "entering their dreams. Saito offers to wipe "  \
                "clean Cobb's criminal history as payment for " \
                "performing an inception on his sick competitor's son.",
            "release_year": 2001
        }
        film = Film.objects.create(**film_data)
        # (4)
        payload = {
            "title": "Inception",
            "release_year": 2010
        }

        response = self.client.patch(
            reverse("film:film-detail", args=[new_film.id])
        )
        # (5)
        film.refresh_from_db()

        self.assertEquals(status.HTTP_200_OK, response.status_code)
        for k, v in payload.items():
            self.assertEquals(v, response.data[k])
            self.assertEquals(v, getattr(film, k))
```

That's a mouthful. Take a minute to read through it. In a nutshell, we are accessing the create and update facilities of our Film model — the difference is that it sits behind our API and we interact with it through HTTP verbs (`POST` and `PATCH`). We can see that:

1. We prepare a payload of data that we would like to create a film with and `POST` it to the `films:film-list` endpoint.
2. A neat trick: we loop through the keys in our payload and compare the response body. The tedious alternative is to write assertions for each field manually.
3. As for the editing endpoint, we first create a film to test against.
4. Then we `PATCH` through the fields that we would like to be updated against the `films:film-detail` endpoint.
5. We then refresh the film instance and check if the changes have been persisted.

### Test the Delete Endpoint

Lastly, let's see how we'll test the Delete endpoint:

```python
# ...


class FilmViewsTest(APITestCase):
    # ...

    def test_can_delete_a_film(self):
        response = self.client.delete(
            reverse("films:film-detail", args=[self.film.id])
        )

        self.assertEquals(status.HTTP_204_NO_CONTENT, response.status_code)
        self.assertFalse(Film.objects.filter(pk=self.film.id))
```

The code should be self-explanatory. It's a pity that there's no `assertIsNotEmpty` available in the unittest module, so we have to resort to the closing `assertFalse` call to check that the filtered queryset is empty. It logically works, but it violates the law of least surprise<sup><a href="#3">[3]</a></sup>.

## Conclusion


In this article, we have touched on the following points:

- What the Django REST Framework is, and how it helps us in API development and testing.
- Using `ModelViewSet` and `ModelSerializer` to build a set of REST-ful BREAD endpoints. 
- How to test each endpoints by issuing HTTP requests through an `APITestCase`.

I hope you get something out of this article. May your endpoints stay REST-ful and your breads loaves fresh. Stay safe!

## Footnotes

- <sup><span id="1">[1]</span></sup> [Move Over Content, Data is King](https://www.technative.io/move-over-content-data-is-king/)
- <sup><span id="2">[2]</span></sup> [BREAD, not CRUD](http://paul-m-jones.com/post/2008/08/20/bread-not-crud/)
- <sup><span id="3">[3]</span></sup> [Principle of Least Astonishment](https://en.wikipedia.org/wiki/Principle_of_least_astonishment)