Back

Creating a Django/React Application with TDD Part 1

created: June 19, 2017, 4:02 p.m.

React
Django
TDD
Django REST

Introduction

This is my first tutorial, so bare with me. There are a lot of tutorials out there for setting up React with Django, so why am I writing this one? Well the answer is I have not seen any of them implement unit testing with it. Although, other guides serve as a "Quick and easy way to get started", mine will be adding unit testing. Also, I will try to make this guide simple and easy. This guide will be building a TODO app in 2 parts:

  1. Building our API with the Django Rest Framework wtihTDD
  2. Implementing a React within our project with TDD

I also offer a completed github repository of everything in this part of the tutorial. Check it out here

About Me

If you don't know me, let me introduce myself. I am Matthew Dickens a Software, Web, and Android developer who at the time of posting recently graduated from the University of Houston. I have been doing Web Development for over 4 years and working with Django for 3 years. I am also skilled in a variety of things such as Android Development. If you wanna know more feel free to visit my website. Also, at the time of this post, I am looking for work so if you know of any potential positions, please let me know.

Part 1: Building our API with the Django Rest Framework and TDD


Starting our Django Project

Django offers tons of tools and libraries that you can use at your disposal. It is one of my favorite web frameworks and I use it a lot. It helps developers develop applications quickly, it is flexible and offers a variety of security options. This guide assumes that you have Python 3.6 with pip installed. If you don't, there are plenty of guides online that tell you how to. So first of all, let us start our Django Application! Open your terminal and type the following bash commands in whatever directory you want your project to be:

  1. Install the necessary tools first
$ pip install virtualenv django
  1. Create a directory for your project
$ mkdir react_django
  1. Move to that directory
$ cd react_django
  1. Use the django-admin startproject command to create your project inside that directory(Dont forget the dot at the end)
$ django-admin startproject react_django .  

Now your project should look like this:

./react_django
|- /react_django
|-- __init__.py 
|-- settings.py # Django settings 
|-- urls.py # Base urls.py file for routes
|-- wsgi.py
|- manage.py # Helps manage your django application

If it looks like this, congrats! You are ready for the next steps in setting up our application.

Creating our virtual environment and installing all requirements

Now let us create a virtual environment to isolate our project from the global site-packages:

$ virtualenv venv

Now your project should look like this:

./react_django
|- /react_django
|-- __init__.py 
|-- settings.py # Django settings 
|-- urls.py # Base urls.py file for routes
|-- wsgi.py
|- /venv # Virtual enviorment directory
|- manage.py # Helps manage your django application

Lets put that virtual environment to use by activating it. I have provided the commands for both Windows and Unix based operating systems:

  • Windows:
$ source venv/Scripts/activate
  • Linux/OSX:
$ source venv/bin/activate

Now that we have activated our virtual environment, it is time to get the libraries we need for this project. First, we need to make a file called requirements.txt

(venv) $ touch requirements.txt

Some of you know where I am going with this, but within the file, put the following content within it:

requirements.txt

Django
djangorestframework
django-webpack-loader
wheel
coverage

As for those who don't know, requirements.txt is a list of libraries that are required to be installed by our environment. By putting this command in the terminal

pip install -r requirements.txt

pip will recursively install every library listed within the requirements.txt file. This gives you the ability to work with others or move your project to different machines without having to remember which libraries to use. I will explain what each library listed in the requirements.txt is and what it serves for our project:

  • Django
  • The backend framework we are using to build our application
  • Source
  • djangorestframework
  • Another framework that provides a powerful and flexible toolkit for building APIs within Django
  • Source
  • django-webpack-loader
  • You will see why we installed this more in the second part of this tutorial. Basically, this library helps us to keep track of the bundles that webpack generates for us.
  • Source
  • wheel
  • A built-package format for Python. You don't really need to know anything about this one. It just does cool things that are a bit complex for this tutorial.
  • coverage
  • We will use this to provide us code coverage when unit testing our backend. It offers a lot of neat features and it what I use personally all the time.
  • selenium
  • Selenium helps us automate the browser for testing purposes. Think of it as a tool to enable us to test site behavior as a client.

Editing the settings.py file

Now that we have everything installed, its time to get the settings.py within the react_django directory sorted out. Here is what the settings.py file should look like

./react_django/settings.py

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = <YOUR_SECRET_KEY>

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    #Third Party Libraries
    'rest_framework', #The library used to serve our api
    #Local Apps
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'react_django.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], #This is where our single html file will be.
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'react_django.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
"""
For this tutorial, we are just going to use sqlite. You can use whatever database you want,
but just make sure you create a test database.
"""
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
        'TEST': {
            #This is so that we have a separate database for our unit testing
            'NAME': os.path.join(BASE_DIR, 'db_test.sqlite3')
        }
    }   
}


# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/

STATIC_URL = '/static/'

Adding a Templates Directory with our single HTML file

Now, if you notice in the settings.py file, I had this for the TEMPLATES setting:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], #This is where our single html file will be.
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

This tells django to look for html within a templates directory at the base directory of our project. So lets add that directory:

$ mkdir templates

And then add a file called index.html to our templates directory which will serve the single HTML file our React application will use:

$ touch templates/index.html

Now your project should look like this:

./react_django
|- /react_django
|-- __init__.py 
|-- settings.py # Django settings 
|-- urls.py # Base urls.py file for routes
|-- wsgi.py
|- /venv # Virtual enviorment directory
|- /templates
|-- index.html
|- manage.py

Setting up and writing our first functional test

Setup for our functional test

Some of you may be thinking, why the are we even using selenium, isn't that going to be tested with our front-end tests? Well, React is not serving the HTML nor the JS, but Django is. So we need to write tests to ensure that we are serving the correct content needed for our application to run on the front-end.

First of all, we need a few things. A driver and a web browser that is compatible with the driver. For the purposes of this tutorial, we will be using the ChromeDriver along with the Google Chrome browser. But you can use other drivers like the gekodriver for Firefox if you wish.

To install the driver, download the latest version that is compatible with your OS here. After you have done that extract the zip, there should be a file called chromedriver that is extracted from the zip file. Now you need to put it somewhere on your system path:

  • for Unix based OS's, put it in ~/.local/bin
  • for Windows, put it in your Python Scripts folder

To test to see if its working, simply do this:

$ chromedriver --version
ChromeDriver 2.30.477700 (0057494ad8732195794a7b32078424f92a5fce41)

Now, if you don't already, download and install google chrome here.

Implementing our functional test

Now, its time to test! First we need to create a directory for our functional test along with our tests.py file:

$ mkdir functional_tests
$ touch functional_tests/__init__.py
$ touch functional_tests/tests.py

Now your project should look like this:

./react_django
|- /react_django
|-- __init__.py 
|-- settings.py # Django settings 
|-- urls.py # Base urls.py file for routes
|-- wsgi.py
|- /venv # Virtual enviorment directory
|- /templates
|-- index.html
|- /functional_tests
|-- tests.py
|- manage.py

Next, lets start our functional test by editing the file we just created:

django_react/functional_tests/tests.py

from django.test import LiveServerTestCase
from selenium import webdriver

class FunctionalTest(LiveServerTestCase):

    #Our setup method which sets the browser to Chrome
    def setUp(self):
        self.browser = webdriver.Chrome()

    def tearDown(self):
        self.browser.quit()

    def test_page_loads_with_react_app(self):
        #Gets the URL of the first page and requests it
        self.browser.get(self.live_server_url)
        #Checks if React Django is in the browser title
        self.assertIn('React Django', self.browser.title)
        #Checks if the <div id="app"></div> exists
        react_app_element = self.browser.find_element_by_id('app')
        self.assertIsNotNone(react_app_element)

Now, lets run the test!

$ python manage.py test
F
======================================================================
FAIL: test_page_loads_div_with_id_app (functional_tests.tests.FunctionalTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\madmin\Desktop\react_django\functional_tests\tests.py", line 18, in test_page_loads_div_with_id_app
    self.assertIn('React Django', self.browser.title)
AssertionError: 'React Django' not found in ''

----------------------------------------------------------------------
Ran 1 test in 7.384s

FAILED (failures=1)
Creating test database for alias 'default'...
Destroying old test database for alias 'default'...
System check identified no issues (0 silenced).
Destroying test database for alias 'default'...

Great! We have a failing test! Now let us write the minimum viable code to get it to pass. First of all, we don't have our HTML wired up to our main urls.py file. So let us do that:

react_django/react_django/urls.py

from django.conf.urls import url
from django.contrib import admin
from django.views.generic import TemplateView

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$', TemplateView.as_view(template_name='index.html'))
]

Next, we need to write the HTML to get this to pass:

react_django/templates/index.html

<html>
    <head>
        <title>React Django</title>
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>

And when we run our tests:

$ python manage.py test
.
----------------------------------------------------------------------
Ran 1 test in 7.241s

OK
Creating test database for alias 'default'...
Destroying old test database for alias 'default'...
System check identified no issues (0 silenced).
Destroying test database for alias 'default'...

Yay! We have a passing test! Now we can move on with building our API.

Building our API with TDD

In this tutorial, we will build a simple TODO list app. So, let us think what we need to do in order to design this API. Let's think of the following user stories:

  • As a user, I would want to see a list of things to do that have not been done
  • As a user, I want to add an item to my TODO list
  • As a user, I want to be able to edit an item on my TODO list
  • As a user, I want to be able to delete an item on my TODO list
  • As a user, I want to be able to mark an item as done
  • As a user, I want to see a list of all items including items that have been complete

Great! This gives us a sense of what features we want to implement. Now, lets start off by creating our app which will handle our TODO List API. Enter the following command in the terminal:

$ python manage.py startapp todo_list_api

Once you run this, your project should look like this:

./react_django
|- /react_django
|-- __init__.py 
|-- settings.py # Django settings 
|-- urls.py # Base urls.py file for routes
|-- wsgi.py
|- /venv
|- /templates
|-- index.html
|- /functional_tests
|-- tests.py
|- /todo_list_api
|-- /migrations
|-- __init__.py
|-- admin.py
|-- apps.py
|-- models.py
|-- tests.py
|-- views.py
|- manage.py

Now, let us not forget to Obey The Testing Goat and write our tests for our API first:

react_django/todo_list_api/tests.py

import json

from django.urls import reverse
from django.http import JsonResponse

from rest_framework import status
from rest_framework.test import APITestCase

from .models import Item


class TodoListApiTest(APITestCase):
    """
    A Test Suite in which tests our API
    """

    def setUp(self):
        """
        The setUp method for the TodoListAPI unit tests
        """
        Item.objects.create(name="Test Item", done=False)
        Item.objects.create(name='Done Item', done=True)

    def tearDown(self):
        """
        The
        :return None:
        """
        Item.objects.filter(name__exact="Test Item").delete()
        Item.objects.filter(name__exact="Done Item").delete()

    def test_get_all_items(self):
        """
        A test in which tests if you can get all items whether they are done or not
        """
        url = reverse('todo-list')
        data = {
            'all': 'y'
        }
        response = self.client.get(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(json.loads(response.content.decode('utf8')),
                         [
                             {'id': 1, 'name': 'Test Item', 'done': False},
                             {'id': 2, 'name': 'Done Item', 'done': True}
                         ])

    def test_get_all_not_done_items(self):
        url = reverse('todo-list')
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(json.loads(response.content.decode('utf8')),
                         [{'id': 1, 'name': 'Test Item', 'done': False}])

    def test_create_item(self):
        """
        A test in which tests item creation
        """
        url = reverse('todo-list')
        data = {
            'name': 'Thing To Do',
            'done': False
        }
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Item.objects.count(), 3)
        self.assertEqual(Item.objects.get(name__exact='Thing To Do').name, 'Thing To Do')
        self.assertEqual(Item.objects.get(name__exact='Thing To Do').done, False)

    def test_get_single_item(self):
        """
        A test which tests the APIs ability to get a single item
        """
        url = reverse('todo-item', kwargs={'pk': 1})
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(json.loads(response.content.decode('utf8')),
                         {'id': 1, 'name': 'Test Item', 'done': False})

    def test_mark_item_as_done_and_edit_name(self):
        """
        A test in which marks an item as done
        """
        url = reverse('todo-item', kwargs={'pk': 1})
        data = {
            'name': 'Clean my room',
            'done': True
        }
        response = self.client.put(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(json.loads(response.content.decode('utf8')),
                         {'id': 1, 'name': 'Clean my room', 'done': True})

    def test_deleting_an_item(self):
        """
        A test for deleting an item
        """
        Item.objects.create(name='Deleted Item', done=False)
        url = reverse('todo-item', kwargs={'pk': 3})
        response = self.client.delete(url)
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertFalse(Item.objects.filter(pk=3).count())

Wow, that's a lot of tests! But for the sake of keeping this tutorial as short as possible, I wanted to write all the tests ahead of time. When you run the tests here, it should fail automatically with tons of syntax errors and import errors. But, think of unit testing as a "shopping list" you write down all the things you need the code to do. So let's write the code to make all these tests pass. According to our tests, our model has mainly 2(except the ID) pieces of data. So our "shopping list" says we need a model to hold a name and whether or not that item is done. So let us create a model that does that:

react_django/todo_list_api/models.py

from django.db import models


class Item(models.Model):
    name = models.CharField(max_length=150, default="")
    done = models.BooleanField(default=False)

Then we need to make migrations and migrate. This is so we can have a migrations file within the migrations directory and migrate our default database from our settings.py:

(venv) $ python manage.py makemigrations
(venv) $ python manage.py migrate

Awesome! Now, since we are working with the rest framework, we need to create ourselves a ModelSerializer for this model. To do that, let us first create a file called serializers.py within our app directory and while we are at it make a urls.py file too:

$ touch todo_list_api/serializers.py && touch todo_list_api/urls.py

Now your app directory should look like this:

|- /todo_list_api
|-- /migrations
|-- admin.py
|-- apps.py
|-- models.py
|-- serializers.py
|-- tests.py
|-- urls.py
|-- views.py

Cool beans! Now let us do some magic on that serializers.py file we just made:

from rest_framework import serializers
from .models import Item


class ItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = Item
        fields = ('id', 'name', 'done')

Didn't expect so little code did you? The REST Framework makes creating a working API very easy! Now that we have a serializer for our Item model, let us add our views in the views.py file:

react_django/todo_list_api/views.py

from django.http import JsonResponse, HttpResponse
from rest_framework import status
from rest_framework.parsers import JSONParser

from .models import Item
from .serializers import ItemSerializer


def todo_list(request):
    """
    A list API view for the TODO list
    :param request:
    :return HttpResponse|JsonResponse:
    """
    if request.method == 'GET':
        is_all = request.GET.get('all')
        if is_all == 'y':
            items = Item.objects.all()
        else:
            items = Item.objects.filter(done=False)
        serializer = ItemSerializer(items, many=True)
        return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK)
    elif request.method == 'POST':
        data = JSONParser().parse(request)
        serializer = ItemSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=status.HTTP_201_CREATED)
    return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


def todo_item(request, pk):
    """
    A single item API view
    :param request:
    :param pk:
    :return HttpResponse|JsonResponse:
    """
    try:
        item = Item.objects.get(pk=pk)
    except Item.DoesNotExist:
        return HttpResponse(status=404)

    if request.method == 'GET':
        serializer = ItemSerializer(item, many=False)
        return JsonResponse(serializer.data, status=status.HTTP_200_OK)
    elif request.method == 'PUT':
        data = JSONParser().parse(request)
        serializer = ItemSerializer(data=data, instance=item)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=200)
        return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    elif request.method == 'DELETE':
        item.delete()
        return JsonResponse({'success': True}, status=status.HTTP_204_NO_CONTENT)

In our tests, it says we need a list API view and a single item API view. So we needed to create 2 views todo_item and todo_list. After that, all we gotta do is just hook it up with the todo_list_api/urls.py and the react_djagno/urls.py files:

todo_list_api/urls.py

from django.conf.urls import url
from .views import todo_list, todo_item

urlpatterns = [
    url(r'^items/$', todo_list, name='todo-list'),
    url(r'^item/(?P<pk>[0-9]+)', todo_item, name='todo-item')
]

react_django/urls.py

from django.conf.urls import url, include #Add include in import statement
from django.contrib import admin
from django.views.generic import TemplateView

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$', TemplateView.as_view(template_name='index.html')),
    url(r'^api/', include('todo_list_api.urls')) #Add this line
]

Now, let us run our tests again:

(venv) $ python manage.py test
Creating test database for alias 'default'...
Destroying old test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 7.720s

OK
Destroying test database for alias 'default'...

Awesome! Now let's see the fruits of our labor and check the code coverage. Remember when we installed that coverage library? Wel will now set that up by adding our .coveragerc file to the base directory our project:

$ touch .coveragerc

Now the project should look like this:

./react_django
|- /functional_tests
|- /react_djagno
|- /templates
|- /todo_list_api
|- /venv
|- .coveragerc
|- db.sqlite3
|- manage.py
|- requirements.txt

Once you created this file, edit it so it looks like this:

./react_django/.coveragerc

[run]
branch = True
source = .

[report]
exclude_lines =
  pragma: no cover

    # Don't complain about missing debug-only code:
    def __unicode__
    def __repr__
    if self\.debug

    # Don't complain if tests don't hit defensive assertion code:
    raise AssertionError
    raise NotImplementedError

    # Don't complain if non-runnable code isn't run:
    if 0:
    if __name__ == .__main__.:

omit =
    react_django/*
    */__init__.py
    functional_tests/*
    */migrations/*
    */site-packages/*
    */Lib/*
    */lib/*
    manage.py
    */tests.py
    */urls.py
    venv/*

show_missing = True

Great! Now, to run the tests with code coverage enter this command:

(venv) $ coverage run manage.py test

This will generate a .coverage file that looks like a bunch of gibberish. In order to see the coverage report in a clean formatted way, just enter this command:

(venv) $ coverage report
Name                           Stmts   Miss Branch BrPart  Cover   Missing
--------------------------------------------------------------------------
todo_list_api\admin.py             1      0      0      0   100%
todo_list_api\apps.py              3      3      0      0     0%   1-5
todo_list_api\models.py            4      0      0      0   100%
todo_list_api\serializers.py       6      0      0      0   100%
todo_list_api\views.py            38      4     16      4    85%   29, 41-42, 53, 23->29, 26->29, 50->53, 54->exit
--------------------------------------------------------------------------
TOTAL                             52      7     16      4    84%

Whoa, looks like we are missing some lines in our views.py file. Let's try to fix that. Add these tests to the tests.py file:

./react_django/todo_list_api/tests.py

    def test_create_item_with_invalid_data(self):
        """
        A test in which tests creating an item with invalid data
        """
        url = reverse('todo-list')
        data = {
            'name': False,
            'done': 'Some Text'
        }
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

    def test_get_invalid_item(self):
        url = reverse('todo-item', kwargs={'pk': 200})
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

    def test_edit_item_with_invalid_data(self):
        url = reverse('todo-item', kwargs={'pk': 1})
        data = {
            'name': False,
            'done': 'Some Text'
        }
        response = self.client.put(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

Now run the tests again with coverage:

(venv) $ coverage run manage.py test
Creating test database for alias 'default'...
Destroying old test database for alias 'default'...
System check identified no issues (0 silenced).
..........
----------------------------------------------------------------------
Ran 10 tests in 7.312s

OK
Destroying test database for alias 'default'...

(venv) $ coverage report
Name                           Stmts   Miss Branch BrPart  Cover   Missing
--------------------------------------------------------------------------
todo_list_api\admin.py             1      0      0      0   100%
todo_list_api\apps.py              3      3      0      0     0%   1-5
todo_list_api\models.py            4      0      0      0   100%
todo_list_api\serializers.py       6      0      0      0   100%
todo_list_api\views.py            38      0     16      2    96%   23->29, 54->exit
--------------------------------------------------------------------------
TOTAL                             52      3     16      2    93%

Now we have increased our coverage by 11% in our views.py file. But the reason why we are at 96% right now is that there can never be a time where those lines jump to the end of the function. So let's change a few things up. Add these tests to the tests.py file:

./react_django/todo_list_api/tests.py

    def test_send_bad_request_to_todo_list_view(self):
        url = reverse('todo-list')
        response = self.client.delete(url)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

    def test_send_bad_request_to_todo_item_view(self):
        url = reverse('todo-item', kwargs={'pk': 1})
        response = self.client.post(url)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

Run the test and it will fail again saying that the serializer is not initialized. Why not move the JsonResponses with the errors as the parameters for the data to where makes more sense sense. Modify your views.py to look like this:

./react_django/todo_list_api/views.py

def todo_list(request):
    """
    A list API view for the TODO list
    :param request:
    :return HttpResponse|JsonResponse:
    """
    if request.method == 'GET':
        is_all = request.GET.get('all')
        if is_all == 'y':
            items = Item.objects.all()
        else:
            items = Item.objects.filter(done=False)
        serializer = ItemSerializer(items, many=True)
        return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK)
    elif request.method == 'POST':
        data = JSONParser().parse(request)
        serializer = ItemSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=status.HTTP_201_CREATED)
        return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    return HttpResponse(status=status.HTTP_400_BAD_REQUEST)

def todo_item(request, pk):
    """
    A single item API view
    :param request:
    :param pk:
    :return HttpResponse|JsonResponse:
    """
    try:
        item = Item.objects.get(pk=pk)
    except Item.DoesNotExist:
        return HttpResponse(status=404)

    if request.method == 'GET':
        serializer = ItemSerializer(item, many=False)
        return JsonResponse(serializer.data, status=status.HTTP_200_OK)
    elif request.method == 'PUT':
        data = JSONParser().parse(request)
        serializer = ItemSerializer(data=data, instance=item)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=200)
        return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    elif request.method == 'DELETE':
        item.delete()
        return JsonResponse({'success': True}, status=status.HTTP_204_NO_CONTENT)
    return HttpResponse(status=status.HTTP_400_BAD_REQUEST)

Let us run our tests with coverage again and see what the result is:

(venv) $ coverage run manage.py test
Creating test database for alias 'default'...
Destroying old test database for alias 'default'...
System check identified no issues (0 silenced).
............
----------------------------------------------------------------------
Ran 12 tests in 7.337s

OK
Destroying test database for alias 'default'...

(venv) $ coverage report
Name                           Stmts   Miss Branch BrPart  Cover   Missing
--------------------------------------------------------------------------
todo_list_api\admin.py             1      0      0      0   100%
todo_list_api\apps.py              3      3      0      0     0%   1-5
todo_list_api\models.py            4      0      0      0   100%
todo_list_api\serializers.py       6      0      0      0   100%
todo_list_api\views.py            40      0     16      0   100%
--------------------------------------------------------------------------
TOTAL                             54      3     16      0    96%

Huzzah! 100% coverage in the views.py file. All we had to do is change the code to make sure every branch is ran. If you so desire to cover the apps.py file, just add this to your tests.py:

./react_django/todo_list_api/tests.py

    def test_app_name_todo_list_api(self):
        self.assertEqual(TodoListApiConfig.name, 'todo_list_api')

Although, it is not necessarily needed, unless you are a perfectionist like me. You should have this after you do this:

(venv) $ coverage run manage.py test
Creating test database for alias 'default'...
Destroying old test database for alias 'default'...
System check identified no issues (0 silenced).
.............
----------------------------------------------------------------------
Ran 13 tests in 7.322s

OK
Destroying test database for alias 'default'...

(venv) $ coverage report

Name                           Stmts   Miss Branch BrPart  Cover   Missing
--------------------------------------------------------------------------
todo_list_api\admin.py             1      0      0      0   100%
todo_list_api\apps.py              3      0      0      0   100%
todo_list_api\models.py            4      0      0      0   100%
todo_list_api\serializers.py       6      0      0      0   100%
todo_list_api\views.py            40      0     16      0   100%
--------------------------------------------------------------------------
TOTAL                             54      0     16      0   100%

If you see this, congratulations! You have scaffolded a working API with unit testing! In the next part of this tutorial, we will do the same thing only with using ReactJS. So what did we do in this tutorial:

  1. We set up a Django Rest environment
  2. We made a functional test to check all the requirements that React needs for later
  3. We created a Restful API using TDD using Django, Django rest framework, and coverage tools

Stay tuned for part 2! Follow me on twitter for more updates. If you have any questions, please leave a comment below. Thanks for reading!


Write a comment


Comments

nsgjuucyday - posted on: Sep 10, 2017 6:22 a.m.

payday loan interest rates <a href="http://paydayloans2017.com"> payday loans near me</a> &lt;a href=&quot;http://paydayloans2017.com&quot;&gt; payday loans near me&lt;/a&gt; http://paydayloans2017.com - payday loans online no credit check payday loan interest rates

Reply

Matt - posted on: Aug 23, 2017 8:28 p.m.

WOW THATS A LOW PRICE!

Reply

Felicitas - posted on: Aug 21, 2017 4:28 a.m.

cheap auto insurance quotes cheap car insurance quotes cheap car insurance online cheap auto insurance

Reply

JohnnieJal - posted on: Aug 03, 2017 1:26 p.m.

Downloads WEB Scene Music FLAC/mp3 1990-2017 Private FTP https://0daymusic.org

Reply

@stevehwg - posted on: Jun 21, 2017 2:36 p.m.

I'll be checking out part 2 :)

Reply

Matthew Dickens - posted on: Jun 20, 2017 12:30 p.m.

Glad you like it! I will hopefully have it finished pretty soon. Make sure you follow me on Twitter for updates.

Reply

@gonzalo_mc - posted on: Jun 19, 2017 9:41 p.m.

Hi. Thanks for the article. I'll be waiting the second part from Chile !!.

Reply