Mock it Til’ You Make It: Test Django with mock and httpretty

January 29, 2018
Written by
Ersel Aker
Contributor
Opinions expressed by Twilio contributors are their own

python_teaser

In this tutorial, we’ll learn how to use Python’s mock and httpretty libraries to test the parts of a Django app that communicate with external services. These could be 3rd party APIs, web sites you scrape, or essentially any resource which you don’t control and is behind a network boundary. In addition to that, we’ll also take a brief look into Django’s RequestFactory class and learn how we can use it to test our Django views. 🕵

In order to illustrate how all these pieces fit together, we’ll write a simple Django app —  “Hacker News Hotline”. It will fetch today’s headlines from Hacker News and serve them behind a TwiML endpoint, so anyone calling our Twilio number can hear the latest headlines on Hacker News.

Right, sounds like a plan. Let’s do this!

Drake wants to use mock and httpretty for Django testing

Python Environment Setup

Before we get our hands dirty, there are a few software packages we need to install.

1) First, we’ll need Python 2.7. Unfortunately httpretty doesn’t have official support for Python 3.x, as of now 😢. You can follow steps outlined in this StackOverflow post to install version 2.7. If you already have Python installed, you can quickly check which version you have by typing the command below in your terminal. Any 2.x version will work to follow this tutorial.

python -V

2) I recommend using virtualenv to keep your development machine tidy. Virtualenv gives you a virtual Python environment where all project dependencies can be installed to.

We’ll also need pip. Pip is a package manager for Python. You can follow this tutorial to set up both virtualenv and pip.

3) Ngrok is a reverse proxy solution, or in simple terms, software which exposes your development server to the outside world. You can install it here.

Last but not least, you’ll also need a Twilio account and a Twilio number with voice capability to test the project. You can sign up for a free trial here.

Django Project Setup

If you have the above environment setup done, let’s move into setting up our Django project.

First, let’s create a directory called twilio-project, then activate virtualenv and install all the dependencies we’ll need; django, httpretty, mock and twilio.

After that let’s start a django project called twilio_voice and add an app called hackernews_calling to our project. We will also need to apply initial database migrations for our Django app.

Open up your terminal and let’s start hacking. 👩‍💻👨‍💻

mkdir twilio-project && cd twilio-project
virtualenv env
source env/bin/activate
pip install django==1.11
pip install httpretty
pip install mock
pip install twilio
django-admin.py startproject twilio_voice .
cd twilio_voice/
django-admin.py startapp hackernews_calling
python manage.py migrate

Great, now we can start writing some code. 👌

Fetching Hacker News Top Stories with Python

We’ll begin with writing a module that fetches top headlines from the Hacker News API. Within the hackernews_calling directory, create a file called hackernews.py with the following code:

"""
This Module Talks to Hacker News API
to fetch latest headlines
"""
import json
import urllib2

def get_headlines(no_of_headlines):
    """
    gets the titles of top stories
    """
    top_story_ids = urllib2.urlopen("https://hacker-news.firebaseio.com/v0/topstories.json").read()

    ids = json.loads(top_story_ids)[:no_of_headlines]
    headlines = []
    for story_id in ids:
        story_url = "https://hacker-news.firebaseio.com/v0/item/{0}.json".format(story_id)
        story = urllib2.urlopen(story_url).read()
        story_json = json.loads(story)
        headlines.append(story_json["title"])
    return headlines

The above module will do the following:

  1. Fetch topstories.json file from remote host
  2. Read a slice of the top_story_ids list
  3. Fetch individual story details
  4. Return a list of headlines

Simple.

Mocking HTTP Requests with Python

So how do we go about testing this module without firing HTTP requests every time we run our tests? Enter httprettyhttpretty is a library which monkey patches Python’s core socket module. It’s perfect for mocking requests and responses with whichever request library you’re using!

By default, Django creates a tests.py where you put all your test cases in one place. I prefer working with a directory structure with multiple test files instead. It helps to keep your tests files granular and concise.

Within the twilio_voice/hackernews_calling directory, apply the following bash magic. 🎩 🐰

rm tests.py 
mkdir tests/ 
touch tests/__init__.py 

Let’s create a test module called test_hackernews.py within the tests directory.

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.test import TestCase
import httpretty
import re # native python regex parsing module
from twilio_voice.hackernews_calling import hackernews

class TestHackerNewsService(TestCase):

    @httpretty.activate
    def test_get_headlines(self):
        # mock for top stories
        httpretty.register_uri(
            httpretty.GET,
            "https://hacker-news.firebaseio.com/v0/topstories.json",
           body="[1,2,3,4,5,6,7,8,9,10]")

        # mock for individual story item
        httpretty.register_uri(
            httpretty.GET,
            re.compile("https://hacker-news.firebaseio.com/v0/item/(w ).json"),
            body="{"title":"some story title"}")

        headlines = hackernews.get_headlines(5);
        self.assertEqual(len(headlines), 5)
        self.assertEqual(headlines[0], 'some story title')
        last_request = httpretty.last_request()
        self.assertEqual(last_request.method, 'GET')
        self.assertEqual(last_request.path, '/v0/item/5.json')

Now let’s investigate our test module closely 🔬.

The first thing you’ll probably notice is the @httpretty.activate decorator that we wrapped around our test method. This decorator replaces the functionality of Python’s core socket module and restores it to its original definition once our test method finishes executing during runtime. This technique is also known as “monkey patching”. Pretty neat, right?

Using httpretty we can register URIs to be mocked and define default return values for testing.

httpretty.register_uri(
    httpretty.GET,
    "https://hacker-news.firebaseio.com/v0/topstories.json",
    body="[1,2,3,4,5,6,7,8,9,10]")

Here we are mocking the /v0/topstories.json endpoint to return a list of numbers — “ids”, from 1 to 10.

It’s also possible to mock URIs using regular expressions with httpretty. We leverage this feature to mock fetching individual story details.

re.compile("https://hacker-news.firebaseio.com/v0/item/(w+).json")

Httpretty also lets us investigate the last request made. We use this feature to verify that the last request made was to fetch the 5th story item’s details.

last_request = httpretty.last_request()
self.assertEqual(last_request.method, 'GET')
self.assertEqual(last_request.path, '/v0/item/5.json')

Let’s run it. Jump back to the project root directory and run the tests.

python manage.py test --keepdb

You should see that test run was successful. 🎉

.
———————————————————————————————————
Ran 1 test in 0.090s

 

TwiML: Talking the Twilio Talk

TwiML is the markup language used to orchestrate Twilio services. With TwiML, you can define how to respond to texts and calls received by your Twilio number. We will generate a TwiML document to narrate the Hacker News data we fetch. When someone calls our Twilio number, they’ll hear the top headlines of the day! 🗣️📱

TwiML is simply an XML document with Twilio specific grammar. For example, to make your Twilio number speak upon receiving a call, you could use the following XML:

<?xml version="1.0" encoding="UTF-8"?> 
<Response> 
    <Say voice="woman">Hello World</Say> 
</Response>

To make things easier we can use the official Twilio Python module to generate TwiML syntax.

Let’s create a new Django view (endpoint) to narrate Hacker News headlines using TwiML. We’ll return top Hacker News story headlines in TwiML format. Our hackernews_calling/views.py should look like this:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.http import HttpResponse
from django.views import View
import hackernews
from twilio.twiml.voice_response import VoiceResponse

class HackerNewsStories(View):
    """
    Hacker News Stories View
    """

    def get(self, request):
        """
        Return top Hacker News story headlines in TwiML format
        """
        headlines = hackernews.get_headlines(5)
        resp = VoiceResponse()
        for headline in headlines:
            resp.say(headline, voice='woman', language='en-gb')

        twiml_str = str(resp)
        return HttpResponse(twiml_str, content_type='text/xml')

To start serving the TwiML doc, we’ll need to register the new endpoint we’ve introduced so our Django app serves it. Change the hackernews_callings/urls.py module to the following:

from django.conf.urls import url
from django.contrib import admin
from twilio_voice.hackernews_calling.views import HackerNewsStories

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^headlines', HackerNewsStories.as_view()),
]

 

Mocking It: Testing Django Views with Mock and RequestFactory

If we were to test the view we have just written, every test run would make HTTP requests as the view is relying on the HackerNews service to fetch the data.

We could use httpretty again to fake requests at the network level, but there is a better solution in this scenario: the mock library we installed earlier. Mock is also part of the Python standard library since v.3.3.

Let’s write our test module and investigate it afterwards. Create a module called test_headlines_view.py under the tests directory with the following content:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.test import TestCase, RequestFactory
import mock
from twilio_voice.hackernews_calling.views import HackerNewsStories

class TestHeadlinesView(TestCase):

    @mock.patch('twilio_voice.hackernews_calling.hackernews.get_headlines', return_value=['headline 1'])
    def test_headlines_xml(self, get_headlines_patched_func):
        request = RequestFactory()
        endpoint = 'headlines'
        get_headlines = request.get(endpoint)
        twiml_response = HackerNewsStories.as_view()(get_headlines)
        self.assertEqual(get_headlines_patched_func.call_count, 1)
        self.assertEqual(twiml_response.status_code, 200)
        expected_content = '<?xml version="1.0" encoding="UTF-8"?><Response><Say language="en-gb" voice="woman">headline 1</Say></Response>' 
        self.assertEqual(twiml_response.content, expected_content)

 

    # snippet provided so reader can follow below explanation easily
    # see file test_headlines_view.py
    @mock.patch('twilio_voice.hackernews_calling.hackernews.get_headlines', return_value=['headline 1'])
    def test_headlines_xml(self, get_headlines_patched_func):
        # lines redacted
        self.assertEqual(get_headlines_patched_func.call_count, 1)

You probably notice the @mock.patch decorator. This decorator monkey patches the function available at the path provided. Similar to how httpretty works, the mocked function is restored to its original state after the test method executes. You should always use absolute paths when working with the mock module as relative paths won’t work!

The second argument provided to the decorator is the value to return when the function is called. You may also have noticed we are passing the get_headlines_patched_func parameter into our test function. This acts as a spy so that we can interrogate if the mocked function was called, how many times it was called, with what arguments it was called and so forth.

    # snippet provided so reader can follow below explanation easily
    # see file test_headlines_view.py
    request = RequestFactory()
    endpoint = 'headlines'
    get_headlines = request.get(endpoint)
    twiml_response = HackerNewsStories.as_view()(get_headlines)
    self.assertEqual(twiml_response.status_code, 200)
    expected_content = 'xml response redacted'
    self.assertEqual(twiml_response.content, expected_content)

Now let’s look into how we use Django’s internal RequestFactory. This module lets us imitate a Django http request object which can be passed into a View class. Using this approach we can test our views without actually hitting any endpoints.

This approach is useful to test any sorting, filtering, pagination or authorization logic within your views. It’s also possible to bake additional request headers into the faked Request object, such as authorization headers for restricted views. In our case, we’re simply mocking a GET request with the headline URI. The status code and response returned is checked afterwards.

Once again, jump back to the project root and run tests again. Everything should still be groovy. 👌

$ python manage.py test --keepdb
..
———————————————————————————————————
Ran 2 tests in 0.098s

 

Putting Our Hacker News App Together

Now that we have our Django app set-up and working, let’s make it work with a Twilio number. (If you haven’t already signed up for Twilio, get a Trial account now.)

We’ll be using Twilio’s “Managing an Incoming Call” flow:

Call infrastructure flow for Python Hacker News headline app

To let Twilio talk to our local Django server, we’ll need to use a reverse proxy such as ngrok. (You can download ngrok here.)

After installing ngrok, start it on port 8000 so the app is publicly available:

$ ngrok http 8000

Pay attention to the output of ngrok, copy the URL provided and add it to the ALLOWED_HOSTS list within settings.py.

Ngrok forwarding URL for Hacker News headline app
ALLOWED_HOSTS = [u'6b40f2a5.ngrok.io']

Now we can run the django app:

$ python manage.py runserver

After starting the Django app, go to Twilio Dashboard > Phone Numbers > Active Numbers and click on a twilio number to set up the webhook URL. Copy and paste the secure forwarding URL (https) provided by ngrok and append /headlines to it. The HTTP action should be set to GET.

Twilio webhook example callback entry

Give your Twilio number a call to listen to today’s Hacker News headlines. Voila! 🎉 🙌

Wrap Up: Hacker News Headlines with Django and Twilio

Congratulations if you’ve made it this far – give yourself a pat on the back! We’ve covered how to use mock, httpretty and RequestFactory modules to easily test Django. You can use mock to replace function bodies at runtime and httpretty to mock http requests at the network level. Both modules leverage monkey patching so mocked functions are restored to their original definitions after the test run. Last but not the least, we have also used RequestFactory to mock requests to test our Django views. You can find the finished project on GitHub.

If you would like to learn more about when you should use mocks, I recommend reading Eric Elliot’s “Mocking is a code smell” post here.

Thanks for reading, please do let me know if you have any questions in the comments.

Ersel Aker is a full-stack developer based in the UK. He has been working with FinTech startups using Python, Nodejs and React. He is also the author of Spotify terminal client  library, and contributor to various open source projects. You can find him on Twitter, GitHub, Medium or at erselaker.com