Skip to content

Assignment 6

Dan Carr edited this page Feb 28, 2017 · 1 revision

Assignment 6 - DRF Fun - Health Check our API

Prerequisites

  • Student has Github account
  • Student has Docker installed.
  • Adequate bandwidth. This is not a cafe exercise.
  • Student has completed Assignment 5

Concepts Introduced

  • Unit Testing and TDD
  • Docker Containers
  • Implement Django Rest Framework API
  • Application Monitoring and Instrumentation

In the previous assignment we set up some basic tests and explored TDD. In this assignment We'll add an endpoint that can monitor the health of our API and underlying services.

Step 0 - Pull the latest version of our class repo

Go to the assignment directory

cd hacku-devops-2017/assign6

Step 1 - Validate that our tests are working

docker-compose run web sniffer

Step 2 - Implement test scaffolding for our health API

APIs should have a health endpoint. This is an endpoint that monitoring software can use to validate the service is still available. The health endpoint should test the availability of any services on which our API relies. For example: SQL databases, services like Redis, ElasticSearch, or downstream APIs.

Set up test for /health returning 200 OK:

In user/tests.py make sure we are importing all the classes we will need:

from django.test import TestCase, Client
from django.contrib.auth.models import User, AnonymousUser
from django.core.urlresolvers import reverse
from django.db import DatabaseError
from mock import patch

Add the following health test case to user/tests.py:

class HealthTestCase(TestCase):
    def setUp(self):
        self.c = Client()

    def test_health_endpoint(self):
        response = self.c.get('/health')
        assert response.status_code == 200, \
            "Expect 200 OK. got: {}" . format (response.status_code)

In user/views.py add our endpoints and register with the router:

class HealthViewSet(viewsets.ViewSet):

	permission_classes = (AllowAny, )

	def list(self, request, format=None):
		data = { "status": "up" }
		return response.Response(data)

and add this line to the end of the list of the routes:

router.register(r'health', HealthViewSet, base_name='health')

Notes:

  • We're using a simple ViewSet here. This is the simplest of the available ViewSet classes. We'll simply override the list() method (GET /<base-name>/).
  • We're going to use the AllowAny permission so this endpoint is open to the web
  • Don't forget to register it! (note: we need to specify a base_name because there is no queryset from which DRF can infer a base-name. We use health. This means that GET /health/ (reverse('health-list') will route to our list() method.

Check your terminal to ensure that we are passing.

Step 3 - Make it more interesting and actually test dependencies

In user/views.py update our HealthViewSet class to do something more interesting:

class HealthViewSet(viewsets.ViewSet):

    permission_classes = (AllowAny, )

    def list(self, request, format=None):

        ## make sure we can connect to the database
        all_statuses = []
        status = "up"

        db_status = self.__can_connect_to_db()

        all_statuses.append(db_status)

        if "down" in all_statuses:
            status = "down"

        data = {
            "data": {
                "explorer" : "/api-explorer",
            },
            "status": {
                "db": db_status,
                "status": status
            }
        }
        return response.Response(data)

    def __can_connect_to_db(self):
        try:
            user = User.objects.first()
            return "up"
        except Exception:
            return "down"

We query the database. If anything goes wrong, we assume the database is down.

In user/tests.py change your HealthTestCase class to:

class HealthTestCase(TestCase):
    def setUp(self):
        self.c = Client()
        self.status_fields = ['db', 'status']

    def test_health_endpoint_ok(self):
        url = reverse('health-list')
        response = self.c.get(url)
        assert response.status_code == 200, \
            "Expect 200 OK. got: {}" . format(response.status_code)

        expected_fields = ["db", "status"]

        for field in self.status_fields:
            assert response.json().get("status", {}).get(field, None) == "up", \
                "Expected field {} to exist" . format(field)

    @patch.object(User.objects, 'first')
    def test_determine_db_status(self, mock_query):
        """Health should not be ok if it cannot connect to the db"""

        mock_query.side_effect = DatabaseError()
        url = reverse('health-list')
        response = self.c.get(url)

        status = response.json().get("status", {})
        db_status = status.get('db')
        assert db_status == 'down', \
            'Expect DB to be down. Got: {}' . format(db_status)

        status = status.get('status')
        assert status == 'down', \
            'Expect status to be down. Got: {}' . format(status)

Notes:

We use patching here: @patch.object(User.objects, 'first') mocks this method. We then specify:

mock_query.side_effect = DatabaseError()

So that when we run the code: User.objects.first() we trigger a DatabaseError(). That's pretty neat, cause now we can make sure that our code does what we expect in the case that there is an issue connecting to the database.

Clone this wiki locally