This tutorial is out of date and no longer maintained.
Code without tests is broken as designed. — Jacob Kaplan-Moss
In software development, testing is paramount. So why should I do it, you ask?
The best way to do code testing is by using Test-Driven Development (TDD).
This is how it works:
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!
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:
Other complementary functionalities that will come later will be:
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 (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.
For DRF to work, you must have:
Create and cd into your project folder. You can call the folder anything you like.
- mkdir projects && $_
Then, create a virtual environment to isolate our code from the rest of the system.
- 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:
- 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
- pip install Django
If you lack pip in your system, simply do:
- sudo easy_install pip
Since we need to keep track of our requirements, we’ll create a requirements.txt
file.
- touch requirements.txt
And add the installed requirements into the text file using pip freeze
- pip freeze > requirements.txt
Then finally, create a Django project.
- 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
Using pip, install DRF
- 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
]
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:
- 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
]
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.
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:
- 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)
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:
- 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:
- 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 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.
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')
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:
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.
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
]
We’ll run our server the Django way with the runserver
command:
- 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:
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"),
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.
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.
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!