Testing and Continuous Integeration

In this chapter we will add test to our API.

DRF provides a few important classes which makes testing APIs simpler. We will be using these classes later in the chapter in our tests.

  • APIRequestFactory: This is similar to Django’s RequestFactory. It allows you to create requests with any http method, which you can then pass on to any view method and compare responses.
  • APIClient: similar to Django’s Client. You can GET or POST a URL, and test responses.
  • APITestCase: similar to Django’s TestCase. Most of your tests will subclass this.

Now lets us write test cases to our polls application.

Creating Test Requests

Django’s ‘Requestfactory’ has the capability to create request instances which allow us in testing view functions individually. Django Rest Framework has a class called ‘APIRequestFactory’ which extends the standard Django’s ‘RequestFactory’. This class contains almost all the http verbs like .get(), .post(), .put(), .patch() et all.

Syntax for Post request:

factory = APIRequestFactory()
request = factory.post(uri, post data)

Lets add a test for the polls list.

from rest_framework.test import APITestCase
from rest_framework.test import APIRequestFactory

from polls import apiviews


class TestPoll(APITestCase):
    def setUp(self):
        self.factory = APIRequestFactory()
        self.view = apiviews.PollViewSet.as_view({'get': 'list'})
        self.uri = '/polls/'

    def test_list(self):
        request = self.factory.get(self.uri)
        response = self.view(request)
        self.assertEqual(response.status_code, 200,
                         'Expected Response Code 200, received {0} instead.'
                         .format(response.status_code))

In the above lines of code, we are trying to access the PollList view. We are asserting that the HTTP response code is 200.

Now run the test command.

python manage.py test

And it will display the below message.

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_list (polls.tests.TestPoll)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/shabda/repos/building-api-django/pollsapi/polls/tests.py", line 19, in test_list
    .format(response.status_code))
AssertionError: 401 != 200 : Expected Response Code 200, received 401 instead.

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)
Destroying test database for alias 'default'...

Ouch! Our test failed. This happened because the view is not accessible without authentication. So we need to create a user and test the view after getting authenticated.

Testing APIs with authentication

To test apis with authentication, a test user needs to be created so that we can make requests in context of that user. Let’s create a test user. Change your tests to

from django.contrib.auth import get_user_model
from rest_framework.authtoken.models import Token
# ...

class TestPoll(APITestCase):
    def setUp(self):
        # ...
        self.user = self.setup_user()
        self.token = Token.objects.create(user=self.user)
        self.token.save()

    @staticmethod
    def setup_user():
        User = get_user_model()
        return User.objects.create_user(
            'test',
            email='testuser@test.com',
            password='test'
        )

    def test_list(self):
        request = self.factory.get(self.uri,
            HTTP_AUTHORIZATION='Token {}'.format(self.token.key))
        request.user = self.user
        response = self.view(request)
        self.assertEqual(response.status_code, 200,
                         'Expected Response Code 200, received {0} instead.'
                         .format(response.status_code))

Now run the test command.

python manage.py test

You should get this response

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.119s

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

Using APIClient

The same test can be written using APIClient. It has get, .post and family. Unlike creating requests first, with APIClient you can GET or POST to a url directly and get a response.

Add a test like this:

from rest_framework.test import APIClient

# ...


class TestPoll(APITestCase):
    def setUp(self):
        self.client = APIClient()
        # ...

    # ...
    def test_list2(self):
        response = self.client.get(self.uri)
        self.assertEqual(response.status_code, 200,
                         'Expected Response Code 200, received {0} instead.'
                         .format(response.status_code))

Let us test it now.

python manage.py test polls.tests.TestPoll


Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_list2 (polls.tests.TestPoll)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/shabda/repos/building-api-django/pollsapi/polls/tests.py", line 37, in test_list2
    .format(response.status_code))
AssertionError: 401 != 200 : Expected Response Code 200, received 401 instead.

----------------------------------------------------------------------
Ran 1 test in 0.136s

FAILED (failures=1)
Destroying test database for alias 'default'...

We are seeing the same failure we saw in the test with APIRequestFactory. You can login a APIClient by calling APIClient.login. Lets update the test.

class TestPoll(APITestCase):
    # ...

    def test_list2(self):
        self.client.login(username="test", password="test")
        response = self.client.get(self.uri)
        self.assertEqual(response.status_code, 200,
                         'Expected Response Code 200, received {0} instead.'
                         .format(response.status_code))
python manage.py test polls.tests.TestPoll
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.260s

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

Voilà! The test passed successfully.

.post and create

We now know how to test our GET APIs. We can use the APIClient with .post method this time.

Let us try creating a new poll by sending the ‘question’, and ‘created_by’ parameters which are needs in the POST method. The test function looks as follows.

class TestPoll(APITestCase):

    # ...
    def test_create(self):
        self.client.login(username="test", password="test")
        params = {
            "question": "How are you?",
            "created_by": 1
            }
        response = self.client.post(self.uri, params)
        self.assertEqual(response.status_code, 201,
                         'Expected Response Code 201, received {0} instead.'
                         .format(response.status_code))

We are asserting that the the http code is 201 if the test passes succesfully. Lets run the tests.

python manage.py test polls.tests.TestPoll.test_create

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.267s

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

Time to celebrate with the API :)

Continuous integration with CircleCI

We have the tests, but we also want it to run on every commit. If you are using Github, CircleCI provides a very well in integrated service to run your tests. We will use Circleci. v2

We can configure our application to use Circle CI by adding a file named .circleci/config.yml which is a YAML(a human-readable data serialization format) text file. It automatically detects when a commit has been made and pushed to a Github repository that is using CircleCI, and each time this happens, it will try to build the project and runs tests. The build failure or success is notified to the developer.

Setting up CircleCI

  • Sign-in: To get started with Circle CI we can sign-in with our github account on circleci.com.
  • Activate Github webhook: Once the Signup process gets completed we need to enable the service hook in the github profile page.
  • Add .circle/config.yml: We should add the yml file to the project.

Writing circle configuration file

In order for circle CI to build our project we need to tell the system a little bit about it. we will be needed to add a file named .circleci/config.yml to the root of our repository. We also need to create a pollsapi/requirements.txt to define our dependencies.

Add this to your pollsapi/requirements.txt

Django==2.0.3
djangorestframework==3.7.7

And then add this to .circleci/config.yml

version: 2
jobs:
  build:
    docker:
      # specify the version you desire here
      - image: circleci/python:3.6.1


    working_directory: ~/repo

    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "pollsapi/requirements.txt" }}
          # fallback to using the latest cache if no exact match is found
          - v1-dependencies-

      - run:
          name: install dependencies
          command: |
            python3 -m venv venv
            . venv/bin/activate
            pip install -r pollsapi/requirements.txt

      - save_cache:
          paths:
            - ./venv
          key: v1-dependencies-{{ checksum "requirements.txt" }}

      - run:
          name: run tests
          command: |
            . venv/bin/activate
            cd pollsapi
            python manage.py test

      - store_artifacts:
          path: test-reports
          destination: test-reports

Below are the important keywords that are used in writting circleci config.yml file.

  • image: Defines the base image including the language and version to use
  • run: It specifies a command which will be run to setup environent and run tests. pip install -r pollsapi/requirements.txt sets up the environment and pip install -r pollsapi/requirements.txt

If everything passed successfully, you should see a green checkmark

_images/circleci.png

Congratulations, you have tests running in a CI environment.

From now onwards whenever we push our code to our repository a new build will be created for it and the tests will run.

We are at the end of the first part of our book. You can read the appendix, which tell about some documentation tools and api consumption tools. Go forward and build some amazing apps and apis.