Open In App

Python Django – Test Driven Development of Web API using DRF & Docker

We are going to create a to-do list web API using Django rest framework, docker and also going to write different tests for different functionalities in our code using test-driven development, but let’s first see what are prerequisites for this project.

Prerequisites :

  1. Docker Installed on your local system
  2. Basic knowledge of python 3.0
  3. Basic knowledge of Django 3.0

Now we are ready to go, let’s learn more about docker and Test-driven development (TDD) and why should we use them.



Docker :

Docker is an open-source containerization platform for automating the deployment of applications as portable, self-sufficient containers that can run on the cloud or on-premises.

Consider a situation where a software engineer writes a code and sends it for testing but that code won’t run on the tester’s local environment because all dependencies are not fulfilled, this problem can be eliminated simply by using docker.



Test-Driven Development (TDD):

Test-Driven Development is a software development practice where the focus is on writing unit tests before writing actual code, it is an iterative approach that combines programming, the creation of unit tests, and refactoring.

Unit Test: Unit tests are the test for testing different functionalities in our code

Three key steps for writing tests:

  1. Setup: creating sample functions that will be used in different test
  2. Execution: Calling the code which is being tested
  3. Assertion: comparing the results with expected results.

Now let’s move to the actual building part.

Creating the project and Setting Up the Dockerfile:

Follow the below steps to create a project and set up the Dockerfile.

Step 1: Create Dockerfile.

Step 2: Populate Dockerfile.

FROM python:3.8-alpine
ENV PYTHONBUFFERED=1 
COPY ./requirements.txt /requirements.txt
RUN pip3 install -r requirements.txt
RUN mkdir /app
WORKDIR /app
COPY ./app /app
RUN adduser -D user
USER user

Step 3: Create a Project folder.

Step 4: Create requirements.txt.

Django==3.2.12
djangorestframework==3.13.1

Step 5: Build Docker image.

$ docker build .

Run above command your docker image must be start building

Step 6: Create docker-compose.




version: "3"
  
services:
    app:
        build: 
            context: .
    ports:
        - "8000:8000" 
    volumes:
        - ./app:/app
    command: >
        sh -c "python3 manage.py runserver 0.0.0.0:8000"

Note: We are using version 3 of docker-compose and our app contains a service called app which is stored in a folder named app we will use port 8000 to run it using the above command.

Step 7: Build docker-compose.

$ docker-compose build

Step 8: Create Django project:

$ docker-compose run app sh -c "django-admin startproject app ."

Step 9: Run Django Server.

$ docker-compose up

Step 10: Create a Django app called “api”.

$ cd app
$ docker-compose run app sh -c "python manage.py startapp api"

Now that we have set up our project we can move to writing actual code but first let’s understand how to write tests in python Django.

Rules for Writing Tests in Django:

Following rules need to be followed while writing a Django test:

Writing Tests for Testing task Model:

Follow the below steps to write the test for the task model:

Step 1: Create a test_models.

Step 2: Writing the first Model test.




from django.test import TestCase
from api import models
  
class ModelTest(TestCase):
  
    def test_tag_model_str(self):
        task = models.Task.objects.create(
            task_name = "Do homework",
            task_description = "Need to complete the homework today",
            task_iscompleted = False
        )
  
        self.assertEqual(str(task), task.task_name)

  1. Above we Imported the required modules, we haven’t create our models yet but we will create them in a second.
  2. Create class ModelTest() and extend it with TestCase
  3. Here we created a unit test to test the model and wrote an assertion to check if the output result is the same as the expected result using assertEqual() which compares the two.

Create Task model:

Follow the below steps to create the task model:

Step 1: In our API app in models.py file include the below code:




from django.db import models
  
class Task(models.Model):
    task_name = models.CharField(max_length=255)
    task_description = models.TextField()
    task_time = models.DateTimeField(auto_now_add=True)
    task_iscompleted = models.BooleanField()
  
    def __str__(self):
        return self.task_name

  1. Create a class and extended it with models.Model.
  2. Write different class fields which represent columns of our model.
  3. At last, we return task_name for verification. 

Step 2: Register Task Model.




from django.contrib import admin
from .models import Task
  
admin.site.register(Task)

Step 3: Migrating changes. 

$ docker-compose run app sh -c "python manage.py makemigrations"
docker-compose run app sh -c "python manage.py migrate"

Step 4: Create Model Serializer.




from rest_framework import serializers
from .models import Task
  
class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = '__all__'

Step 5: Test the task model.

docker-compose run app sh -c "python manage.py test"

Writing test for API Views:

Follow the below steps to write the test for API view:

Step 1: Create a test file.

Step 2: Write the test. 




from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
  
from django.urls import reverse
from api.serializers import TaskSerializer
from api.models import Task
  
  
get_tasks_url = reverse('get')
create_url = reverse('create')
  
def update_url(id):
    return reverse('update', args=[id])
  
def delete_url(id):
    return reverse('delete', args=[id])
  
def details_url(id):
    return reverse('details', args=[id])
  
def sample_payload():
    payload = {
        "task_name" : "Do homework",
        "task_description" : "Need to complete the homework today",
        "task_iscompleted" : False
    }
    return Task.objects.create(**payload)

Above we imported the required modules then we use the reverse function which allows retrieving URL details from urls.py file through the name-value provided there, then we created sample_payload which is a sample task model object creating sample functions that make our code clean and fast.




class TaskApiTest(TestCase):
      
    def setUp(self):
        self.client = APIClient()
  
    def test_get_task_list(self):
        res = self.client.get(get_tasks_url)
          
        self.assertEqual(res.status_code, status.HTTP_200_OK)
  
    def test_create_task(self):
        payload = {
            "task_name" : "Do homework",
            "task_description" : "Need to complete the homework today",
            "task_iscompleted" : False
        }
  
        res = self.client.post(create_url, payload)
  
        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
  
    def test_update_task(self):
        task = sample_payload()
  
        payload = {
            "task_name" : "Do homework",
            "task_description" : "Need to complete the homework today",
            "task_iscompleted" : True
        }
  
        url = update_url(task.id)
        res = self.client.put(url, payload)
  
        task.refresh_from_db()
        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
      
    def test_delete_task(self):
        task = sample_payload()
        url = delete_url(task.id)
  
        res = self.client.delete(url)
  
        self.assertEqual(res.status_code, status.HTTP_202_ACCEPTED)
  
    def test_task_details(self):
        task = sample_payload()
        url = details_url(task.id)
  
        res = self.client.get(url)
  
        self.assertEqual(res.status_code, status.HTTP_200_OK)

Above we have different unit tests for different views, first we create a class and extend it with TestCase and write a function to test different views (notice how every function name starts with test), override setup function from TestCase and use it to initialize class variables, then write code and perform require assertions to compare given output with the expected output.

Writing API Views and URLs:

Follow the below steps to write the API views and URLs:

Step 1: Creating views




from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import Task
from .serializers import TaskSerializer
  
@api_view(['GET'])
def get_all_tasks(request):
    tasks = Task.objects.all().order_by("-task_time")
    serializer = TaskSerializer(tasks, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)
    
@api_view(['GET'])
def get_task_details(request, pk):
    task = Task.objects.get(id=pk)
    serializer = TaskSerializer(task, many=False)
  
    return Response(serializer.data, status=status.HTTP_200_OK)




@api_view(['POST'])
def create_task(request):
    serializer = TaskSerializer(data=request.data)
    if serializer.is_valid():
        serializer.save()
        return Response(serializer.data, status=status.HTTP_201_CREATED)
  
    return Response(status=status.HTTP_400_BAD_REQUEST)

Above is a view for creating a new task first we need to get data from the request and serialize it if serialized data is valid then simply save the serializer and return desired response.




@api_view(['PUT'])
def update_task(request, pk):
    task = Task.objects.get(id=pk)
    serializer = TaskSerializer(instance=task, data=request.data)
  
    if serializer.is_valid():
        serializer.save()
        return Response(serializer.data, status=status.HTTP_201_CREATED)
  
    return Response(status=status.HTTP_400_BAD_REQUEST)
  
@api_view(['DELETE'])
def delete_task(request, pk):
    task = Task.objects.get(id=pk)
    task.delete()
  
    return Response('Deleted successfully',status=status.HTTP_202_ACCEPTED)

Here we are completed with writing views.

Step 2: Configuring URL’s.




from django.urls import path
from . import views
urlpatterns = [
    path('get/', views.get_all_tasks, name="get"),
    path('create/', views.create_task, name="create"),
    path('update/<int:pk>', views.update_task, name="update"),
    path('delete/<int:pk>', views.delete_task, name="delete"),
    path('details/<int:pk>', views.get_task_details, name="details")
]

Above are different URL patterns to make requests to different views and get a response.




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

Here if any URL starts from api/ then transfer control to api.urls.

Final Test:

docker-compose run app sh -c "python manage.py test"

 

Outputs:

$ docker-compose up

 

 

 

 

 

 

Conclusion: 

Thus we learned about creating API with Django rest framework, docker, and test-driven development. As the Software industry is growing so fast it is necessary to be updated with new technologies.


Article Tags :