Test RESTful API In Django Rest Framework

By StartxLabs
Date 20-11-20
Test RESTful API In Django Rest Framework
" Test RESTful API In Django Rest Framework"

Need for Testing:

While developing any application it is quite challenging to find out all the edge cases and scenarios where our code would fail.

To resolve this problem we test our code, and if our code passes all the required testing parameters only then we push our code to the live/production environment. But, manual testing is a very long process and time taking, instead of manual testing, we can automate our testing process by enforcing all the testing parameters on that particular module and see if the code passes all the testing parameters or not without putting too much amount of time in the testing process as compare to the manual testing process.

 

 

API Testing Using Django Rest Framework:

A collection of well-defined test cases is useful to solve, or avoid, a number of problems:

  1. When you’re writing new code, you can use tests to validate your code works as expected.
  2. When you’re refactoring or modifying old code, you can use tests to ensure your changes haven’t affected your API behaviour unexpectedly.

Testing a Restful API is a complex task because an API is made of several layers of logic – from HTTP-level request handling to data validation and processing, to provide the appropriate response. With the django rest framework test-execution framework, you can simulate requests, insert test data, inspect your API output and generally verify your code is doing what it should be doing.

 

A TODO-List:

We'll be creating a TODO list API. the API have the ability to:

  • Create a Task

  • Retrieve a Task

  • Update a Task

  • And, Delete a Task from the TODO List

Setup:

 

Step 1:

File: project/project/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'myapp',
]

 

Step 2:

File: project/myapp/models.py

from django.db import models
from django.contrib.auth.models import User

class TODOList(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=255, unique=True)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name

 

Step 3:

project/myapp/serializers.py

from rest_framework import serializers
from myapp.models import TODOList

class TODOSerializer(serializers.ModelSerializer):
	class Meta:
	   model = TODOList
	   fields = (
            'id', 
            'user',
            'title', 
            'date_created', 
            'date_modified'
          )
          read_only_fields = (
            'date_created',
            'date_modified'
          )

 

Step 4:

File: project/myapp/permissions.py

from rest_framework import permissions

class IsOwner(permissions.BasePermission):
   def has_object_permission(self, request, view, obj):
      return request.user.is_authenticated and request.user.id == obj.user_id

 

Step 5:

File: project/myapp/views.py

from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView  
from rest_framework.permissions import IsAuthenticated
from myapp.models import TODOList
from myapp.serializers import TODOSerializer
from myapp.permissions import IsOwner



class TODOListView(ListCreateAPIView):
	queryset = TODOList.objects.all()
	serializer_class = TODOSerializer
	permission_classes = (IsAuthenticated,)

class TODODetailView(RetrieveUpdateDestroyAPIView):
	queryset = TODOList.objects.all()
	serializer_class = TODOSerializer
	permission_classes = (IsOwner,)

 

Step 6:

File: project/myapp/urls.py

from django.urls import path
from myapp.views import TODOListView, TODODetailView

urlpatterns = [
   path('todo/', TODOListView.as_view(), name='todo'),
   path('todo/<str:pk>/', TODODetailView.as_view(), name='todo-detail')         
]

 

Step 7:

File: project/project/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
   path('admin/', admin.site.urls),
   path('api/v1/', include('myapp.urls'))
]

 

Now, we have created our API’s and added an authentication layer as well for making our API much more secure, For our TODOList application. Let’s create some test cases to test whether our API is working as expected or not. As we usually write test cases with some assertions in the end which would tell us that code is performing as per the desired requirement or not but, In this walkthrough instead of writing assertions we will just capture the test case result into a list of dictionaries and when we run the test case, We will send the captured test case result as a CSV file and email to our peer dev team. The benefit with this approach is, suppose you are working in a team and run the test case, As soon as the test case process gets completed your team gets an email with the test case result specifying that the code you wrote is working as expected or not. So later on when some other teammate is starting working on the same code they do not have to go through the hassle of running the test cases again and validating the code again as it is already done earlier by you.

TEAMWORK RIGHT…!!!!

 

                   


Note: The default startapp template creates a tests.py file in the new application. This might be fine if you only have a few tests, but as your test suite grows you’ll likely want to restructure it into a tests package so you can split your tests into different submodules such as test_models.py, test_views.py, test_forms.py, etc. Feel free to pick whatever organizational scheme you like. For the sake of this article, we can use the already created tests.py

Now, let's first add a utils.py file inside our app and a function for sending the test case result email to a given list of recipients.

 

File: project/myapp/utils.py

import io
import csv
import logging

from django.core.mail import EmailMultiAlternatives
from django.utils import timezone

logger = logging.getLogger('__name__')


def send_test_csv_report(test_results, recipients):
   filename = "test_csv_report.csv"
   string = io.StringIO()
   csv_writer = csv.writer(
     string, 
     delimiter=',', 
     quotechar='"', 
     quoting=csv.QUOTE_MINIMAL
   )

   csv_writer.writerow([
     'S.No', 
     'Test Name', 
     'Test Result', 
     'Test Description'
   ])

   for result_index, result in enumerate(test_results):
       csv_writer.writerow([
         result_index + 1, 
         result['test_name'], 
         result['result'],
         result['test_description']
       ])

   email = EmailMultiAlternatives(
     subject=str(timezone.now().strftime("%d-%m-%Y")) + ' ' +     'Test Results' + " CSV report",
     from_email='[email protected]',
     to=recipients
   )
   email.attach(
     filename=filename, 
     mimetype="text/csv", 
     content=string.getvalue()
   )
   email.send()
   logger.info('Email Sent Successfully!!')

 

After adding the function for sending the email, now let’s create some test cases inside our tests.py file.

File: project/myapp/tests.py

import inspect

from myapp.models import TODOList
from myapp.utils import send_test_csv_report

from django.contrib.auth.models import User
from rest_framework.test import APIClient, APITestCase
from rest_framework.reverse import reverse
from rest_framework import status

TEST_RESULTS = []
RECIPIENTS = ['[email protected]']


class TODOListTestCase(APITestCase):
 def setUp(self) -> None:
   self.user =  User.objects.create_user(username='test_user', password='adminpass')
   self.other_user = User.objects.create_user(username='other_user', password='adminpass')
   self.task = TODOList.objects.create(user=self.user, title='My Initial Task')
self.client = APIClient()

 @classmethod
 def tearDownClass(cls):
   User.objects.filter(username__in=['test_user', 'other_user']).delete()

 def test_create_task_with_un_authenticate_user(self):
   """
   In this Test Case we are testing the TODO Create API using an unauthenticated user.
   """

   response = self.client.post(reverse('todo'), {'title': 'My Task 1'}, format='json')

   is_passed = response.status_code == status.HTTP_403_FORBIDDEN

   TEST_RESULTS.append({
       "result": "Passed" if is_passed else "Failed",
       "test_name": inspect.currentframe().f_code.co_name,
       "test_description": "Un-authenticated user cannot add a task into the TODO List"
   })

  def test_get_other_user_task_detail(self):
   """
   In this Test Case we are testing the TODO GET API, and trying to get task details of  a user using a different user credentials.
   """

   self.client.login(username='other_user', password='adminpass')

   response = self.client.get(reverse('todo-detail', args=[str(self.task.id)]))

   is_passed = response.status_code == status.HTTP_403_FORBIDDEN

   TEST_RESULTS.append({
       "result": "Passed" if is_passed else "Failed",
       "test_name": inspect.currentframe().f_code.co_name,
       "test_description": "Only the Owner can view the Task Detail"
   })

  def test_create_task_with_authenticated_user(self):
   self.client.login(username='test_user', password='adminpass')

   response = self.client.post(reverse('todo'), {'title': 'My Task'}, format='json')

   is_passed = response.status_code == status.HTTP_201_CREATED
   TEST_RESULTS.append({
       "result": "Passed" if is_passed else "Failed",
       "test_name": inspect.currentframe().f_code.co_name,
       "test_description": "Task Added into the TODO List Successfully"
   })

 def test_get_task_detail(self):
   self.client.login(username='test_user', password='adminpass')

   response = self.client.get(reverse('todo-detail', args=[str(self.task.id)]))

   is_passed = response.status_code == status.HTTP_200_OK

   TEST_RESULTS.append({
       "result": "Passed" if is_passed else "Failed",
       "test_name": inspect.currentframe().f_code.co_name,
       "test_description": "Task Detail Retrieved Successfully"
   })

class CSVReportTest(APITestCase):
   def test_send_csv(self):
       send_test_csv_report(
         test_results=TEST_RESULTS,
         recipients=RECIPIENTS
       )

 

Note: 

  • Whenever we create any test case we should always define two important methods first,
    • def setUp(self): This method is used for feeding the initial data that is required by every test method defined in the test case class, so you do not have to define the data everytime inside the test methods again and again.
    • def tearDownClass(cls): The signature of this method is similar to a destructor that is used in the OOPS approach. As when a test case finishes the process at last this method is run and delete/remove all the data that is used by the test case for its execution. Like in the above example we are deleting the users that we created for test cases. So, we should always define this method for removing the data required by a Test Case and keep the state of the DB as it is as before the Test Case ran.
  • All the test methods defined in the test case class should start with the test as a prefix, otherwise, the test module considers the method as a normal class method, not a test method.
  • Whenever you run the test case always use below command. It will sort test cases in the opposite execution order. This may help in debugging the side effects of tests that aren’t properly isolated. And, execute our CSVReportTest after executing all the other test cases
python manager.py test --reverse myapp

 

Test Case Results:

 

CONGRATULATIONS.!!! You have successfully created a structural, well defined test suite for testing the API.

 

Conclusion:

 

Automated testing is an extremely useful bug-killing tool for the modern web developer. Using django rest framework testing module not only give you the ability to test only those API’s which is created via DRF but you can test all the various API’s which were developed using ROR, NodeJS, Spring, Flask etc, and test that API’s is working as expected or not.

DRF testing modules also support Token Authentication as well. So, if you use Token Authentication in your project, then you can do this for making an authenticated request whenever you run your test case:
 

from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient

# Include an appropriate `Authorization:` header on all requests.
token = Token.objects.get(user__username='lauren')
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=Bearer ' + token.key)

 

For More Details:

Readout Django Testing Documentation and Django Rest Framework Testing Documentation

subscribe to startxlabs

startxlabs