Tutorial

Build a REST API with Django – A Test Driven Approach: Part 1

Draft updated on Invalid Date
author

Jee Gikera

Build a REST API with Django – A Test Driven Approach: Part 1

This tutorial is out of date and no longer maintained.

Introduction

Code without tests is broken as designed. — Jacob Kaplan-Moss

In software development, testing is paramount. So why should I do it, you ask?

  • Tests have a short feedback loop, enabling you and your team to learn faster and adjust
  • Less time is spent debugging, allowing you to spend more time writing code
  • Tests act as documentation for your code!
  • They improve code quality while reducing bugs
  • After refactoring code, your tests will tell you whether the change has broken previously working code

The best way to do code testing is by using Test-Driven Development (TDD).

This is how it works:

  • Write a test. – The test will flesh out some functionality in your app
  • Then, run the test – The test should fail since there’s no code to make it pass.
  • Write the code – To make the test pass
  • Run the test – If it passes, you are confident that the code you’ve written meets the test requirements
  • Refactor code – Remove duplication, prune large objects and make the code more readable. Re-run the tests every time you refactor the code
  • Repeat – That’s it!

Being a fan of best practices, we are going to use TDD to create a bucketlist API. The API will have CRUD (Create, Read, Update, Delete) and authentication capabilities. Let’s get to it then!

A Bucketlist

The aim of this article is to help you learn awesome stuff while creating new things. We’ll be creating a bucket list API. If you haven’t heard about the term bucket list, it is a list of all the goals you want to achieve, dreams you want to fulfill, and life experiences you desire to experience before you die (or hit the bucket). This API should therefore help us to create and manage our bucketlists.

To be on the same page, the API should have the ability to:

  • Create a Bucketlist
  • Retrieve a Bucketlist
  • Update it
  • And, Delete one’s bucketlist

Other complementary functionalities that will come later will be:

  • Authentication of API Users
  • Searching Bucketlists
  • Adding bucketlist items into a bucketlist
  • Pagination

New to Python, Django?

If you haven’t done any Django, Check out Building your first Django application. It’s an excellent resource for beginners.

Now that we know about a bucketlist, here’s a bit about the tools we’ll use to create its app.

Django Rest Framework

Django Rest Framework (or simply DRF) is a powerful module for building web APIs. It’s very easy to build model-backed APIs that have authentication policies and are browsable.

Why DRF?

  • Authentication – From basic and session-based authentication to token-based and Oauth2 capabilities, DRF is king.
  • Serialization – It supports both ORM and Non-ORM data sources and seamlessly integrates with your database.
  • Great Documentation – If you get stuck somewhere, you can refer to its vast online documentation and great community support
  • Heroku, Mozilla, Red Hat, and Eventbrite are among the top companies using DRF in their APIs.

Requirements

For DRF to work, you must have:

  • Python
  • Django

Create and cd into your project folder. You can call the folder anything you like.

  1. mkdir projects && $_

Then, create a virtual environment to isolate our code from the rest of the system.

  1. virtualenv -p /usr/local/bin/python3 venv

The -p switch tells virtualenv the path to the python version you want to use. Ensure that you put the correct python installation path after the -p switch. venv is your environment. Even though you can name your environment anything, it’s considered best practice to simply name it venv or env.

Activate your virtual environment by doing this:

  1. source venv/bin/activate

You will see a prompt with the environment name (i.e., (venv)). It means the environment is now active. Now we’re ready to install our requirements inside our environment.

Inside the projects folder, install Django using pip

  1. pip install Django

If you lack pip in your system, simply do:

  1. sudo easy_install pip

Since we need to keep track of our requirements, we’ll create a requirements.txt file.

  1. touch requirements.txt

And add the installed requirements into the text file using pip freeze

  1. pip freeze > requirements.txt

Then finally, create a Django project.

  1. django-admin startproject djangorest

We should now have a folder with the name djangorest created. Feel free to give it any other name. The folder structure should look like this:

djangorest
├─djangorest
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

Integrating DRF

Using pip, install DRF

  1. pip install djangorestframework

For our app to use DRF, we’ll have to add rest_framework into our settings.py. Let’s go right ahead and do that.

# /djangorest/djangorest/settings.py
...

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles', # Ensure a comma ends this line
    'rest_framework', # Add this line
]

Creating the Rest API app

In Django, we can create multiple apps that integrate to form one application. An app in Django is simply a python package with a bunch of files including the __init__.py file.

First, cd into the djangorest directory on your terminal. We do this so that we can access the manage.py file. Then create the app as follows:

  1. python3 manage.py startapp api

The startapp command creates a new app. Our app is called api. It will hold our API logic. So far, you should have a folder named api alongside the djangorest app. To integrate our api app with the djangorest main app, we’ll have to add it to our djangorest settings.py. Let’s go right ahead and do that.

...

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'api', # Add this line
]

Let The Coding Begin

First, we test!

We’d want to create the models first. But we have no tests written. We’ll therefore write some tests in the tests.py folder of our API app.

# /api/tests.py

from django.test import TestCase
from .models import Bucketlist


class ModelTestCase(TestCase):
    """This class defines the test suite for the bucketlist model."""

    def setUp(self):
        """Define the test client and other test variables."""
        self.bucketlist_name = "Write world class code"
        self.bucketlist = Bucketlist(name=self.bucketlist_name)

    def test_model_can_create_a_bucketlist(self):
        """Test the bucketlist model can create a bucketlist."""
        old_count = Bucketlist.objects.count()
        self.bucketlist.save()
        new_count = Bucketlist.objects.count()
        self.assertNotEqual(old_count, new_count)

The code above imports the test case from django.test. The test case has a single test that tests whether the model can create a bucketlist with a name.

Then, we define our models

We need to create a blank model class. This is done in our models.py

# /api/models.py

from django.db import models


class Bucketlist(models.Model):
  pass

Running the test is super easy with Django. We’ll use the test command as follows:

  1. python3 manage.py test

You should see a bunch of errors all over the screen. Don’t worry about it. It’s because we haven’t written our model fields and updated our database yet. Django uses SQlite as its default database so we’ll use it for now. Also, we don’t have to write a single SQL statement when creating the models. Django handles all that for us. In the models.py we’ll define fields that will represent the table fields in our database.

# api/models.py

from django.db import models


class Bucketlist(models.Model):
  """This class represents the bucketlist model."""
    name = models.CharField(max_length=255, blank=False, unique=True)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

    def __str__(self):
        """Return a human readable representation of the model instance."""
        return "{}".format(self.name)

We migrate!

Migrations are Django’s way of propagating changes you make to your models (like adding a field, deleting a model, etc.) into your database schema.

Now that we have a rich model in place, we need to tell the database to create the relevant schema.

In your console, run this:

  1. python3 manage.py makemigrations

This creates a new migration based on the changes we’ve made to our model.

Then, apply the migrations to your DB by doing this:

  1. python3 manage.py migrate

When you run the tests, you should see something like this:

The tests have passed! This means that we can proceed to write the serializers for our app

Serializers

Serializers serialize and deserialize data. So what’s all this about, you ask?

Serializing is changing the data from complex querysets from the DB to a form of data we can understand, like JSON or XML. Deserializing is reverting this process after validating the data we want to save to the DB.

Model Serializers are awesome!

The ModelSerializer class lets you automatically create a Serializer class with fields that correspond to the Model fields. This reduces our lines of code significantly.

Create a file called serializers.py inside the API directory.

Let’s write some code in it:

# api/serializers.py

from rest_framework import serializers
from .models import Bucketlist


class BucketlistSerializer(serializers.ModelSerializer):
    """Serializer to map the Model instance into JSON format."""

    class Meta:
        """Meta class to map serializer's fields with the model fields."""
        model = Bucketlist
        fields = ('id', 'name', 'date_created', 'date_modified')
        read_only_fields = ('date_created', 'date_modified')

Views

We’ll first write the view’s tests. Writing tests seems daunting at first. However, it’s easy to know what to test when you know what to implement.

In our situation, we want to create views that will handle the following:

  • Create a bucketlist – Handle POST request
  • Read a bucketlist(s) – Handle GET request
  • Update a bucketlist – Handle PUT request
  • Delete a bucketlist – Handle DELETE request

Based on the above functionality, we know what to test. We’ll use them as a guide.

Let’s take the first case. If we want to test whether the API will create a bucketlist successfully, we’ll write the following code in tests.py:

# api/tests.py

# Add these imports at the top
from rest_framework.test import APIClient
from rest_framework import status
from django.core.urlresolvers import reverse

# Define this after the ModelTestCase
class ViewTestCase(TestCase):
    """Test suite for the api views."""

    def setUp(self):
        """Define the test client and other test variables."""
        self.client = APIClient()
        self.bucketlist_data = {'name': 'Go to Ibiza'}
        self.response = self.client.post(
            reverse('create'),
            self.bucketlist_data,
            format="json")

    def test_api_can_create_a_bucketlist(self):
        """Test the api has bucket creation capability."""
        self.assertEqual(self.response.status_code, status.HTTP_201_CREATED)

This test fails when we run it. This is ok. It happens so because we haven’t implemented the views and URLs for handling the POST request.

Let’s go ahead and implement them! On views.py, write the following code:

# api/views.py

from rest_framework import generics
from .serializers import BucketlistSerializer
from .models import Bucketlist


class CreateView(generics.ListCreateAPIView):
    """This class defines the create behavior of our rest api."""
    queryset = Bucketlist.objects.all()
    serializer_class = BucketlistSerializer

    def perform_create(self, serializer):
        """Save the post data when creating a new bucketlist."""
        serializer.save()

The ListCreateAPIView is a generic view that provides GET (list all) and POST method handlers

Notice we specified the queryset and serializer_class attributes. We also declare a perform_create method that aids in saving a new bucketlist once posted.

Handling URLs

For it to be complete, we’ll specify URLs as endpoints for consuming our API. Think of URLs as an interface to the outside world. If someone wants to interact with our web API, they’ll have to use our URL.

Create a urls.py file on the API directory. This is where we define our URL patterns.

# api/urls.py

from django.conf.urls import url, include
from rest_framework.urlpatterns import format_suffix_patterns
from .views import CreateView

urlpatterns = {
    url(r'^bucketlists/$', CreateView.as_view(), name="create"),
}

urlpatterns = format_suffix_patterns(urlpatterns)

The format_suffix_pattern allows us to specify the data format (raw JSON or even HTML) when we use the URLs. It appends the format to be used for every URL in the pattern.

Finally, we add a URL to the main app’s urls.py file so that it points to our API app. We will have to include the api.urls we just declared above into the main app urlpatterns.

Go to the djangorest folder and add the following to the urls.py:

# djangorest/urls.py
# This is the main urls.py. It shouldn't be mistaken for the urls.py in the API directory

from django.conf.urls import url, include

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^', include('api.urls')) # Add this line
]

Let’s Run!

We’ll run our server the Django way with the runserver command:

  1. python3 manage.py runserver

You should see this output on your console

That means everything is running smoothly.

Enter the server-specified URL (http://127.0.0.1:8000/bucketlists) in your browser. And viola – It works!

Go ahead and write a bucketlist and click the post button to confirm whether our API works.

You should see something like this:

Reading, Updating and Deletion

Writing the tests

We’ll write three more tests to cater for GET, PUT and DELETE requests. We’ll wrap them as follows:

    # api/tests.py

    def test_api_can_get_a_bucketlist(self):
        """Test the api can get a given bucketlist."""
        bucketlist = Bucketlist.objects.get()
        response = self.client.get(
            reverse('details',
            kwargs={'pk': bucketlist.id}), format="json")

        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertContains(response, bucketlist)

    def test_api_can_update_bucketlist(self):
        """Test the api can update a given bucketlist."""
        change_bucketlist = {'name': 'Something new'}
        res = self.client.put(
            reverse('details', kwargs={'pk': bucketlist.id}),
            change_bucketlist, format='json'
        )
        self.assertEqual(res.status_code, status.HTTP_200_OK)

    def test_api_can_delete_bucketlist(self):
        """Test the api can delete a bucketlist."""
        bucketlist = Bucketlist.objects.get()
        response = self.client.delete(
            reverse('details', kwargs={'pk': bucketlist.id}),
            format='json',
            follow=True)

        self.assertEquals(response.status_code, status.HTTP_204_NO_CONTENT)

If we run these tests, they should fail. Let’s fix that.

It’s time we complete the API with a PUT and DELETE method handlers.

We’ll define a view class for this. On the views.py file, add the following code:

# api/views.py

class DetailsView(generics.RetrieveUpdateDestroyAPIView):
    """This class handles the http GET, PUT and DELETE requests."""

    queryset = Bucketlist.objects.all()
    serializer_class = BucketlistSerializer

RetrieveUpdateDestroyAPIView is a generic view that provides GET (one), PUT, PATCH, and DELETE method handlers.

Finally, we create this new URL to be associated with our DetailsView.

# api/urls.py

from .views import DetailsView

url(r'^bucketlists/(?P<pk>[0-9]+)/$',
        DetailsView.as_view(), name="details"),

Wrapping it up

Enter the specified URL (http://127.0.0.1:8000/bucketlists/1/) in your browser. And voila! – It works! You can now edit the existing bucketlist.

Conclusion

Phew! Congratulations for making it to the end of this article – You are awesome!

In Part 2 of the Series, we’ll delve into adding users, integrating authorization and authentication, documenting the API, and adding more refined tests.

Want to dig deeper? Feel free to read more of DRF’s Official Documentation.

And if you’re new to Django, I find Django For Beginners excellent.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Jee Gikera

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.