Faking the Funk: Mocking External Services in Python Tests

by Dan Riti on July 25, 2014

iStock_000005545558Small

In this day and age, it’s difficult to build an application that does not rely on some type of external service. Whether the service is handling user identity, analyzing interesting data, or hurling RESTful insults, you have to accept the fact that you now have a dependency on something you do not control.

One place where this dependency can become painfully obvious is running your test suite. Making requests to an external service can cause your tests to become extremely slow. According to Harlow Ward, these extra requests during tests can cause a wide range of problems:

  • Tests failing intermittently due to connectivity issues.
  • Dramatically slower test suites.
  • Hitting API rate limits on 3rd party sites (e.g. Twitter).
  • Service may not exist yet (only documentation for it).
  • Service doesn’t have a sandbox or staging server.

We want to ensure our test suite is fast and consistent. Thus, let’s take a look at how we can leverage mocks to create an isolated test environment in Python.

We Want the Funk

To begin, let’s take a look at some of the tools we’re going to use in this exercise:

  1. GitHub API, external service we are going to mock
  2. requests, a fantastic HTTP library
  3. httmock, a mocking library for requests
  4. python-mocked-service, an example GitHub repository for tracking our code changes throughout this article

Let’s start by assuming our application relies on the GitHub repository endpoint. Thus, we can begin with the following changes:

  • (Commit) Add get_repository function, for getting GitHub repository information.
  • (Commit) Add test_get_repository test case, for testing our newly created get_repository function.

Now let’s go ahead and run the test:

 (env)[driti@ubuntu]$ python test_github.py
.
----------------------------------------------------------------------
Ran 1 test in 0.245s

OK

We can see the test passes, however it took a total of 0.245 seconds. I don’t know about you, but that’s way too slow for me. So let’s speed things up a bit by mocking out the repository endpoint! To do this, we can make the following changes:

  • (Commit) Create a repository mock, for faking the response from the repository endpoint.
  • (Commit) Update our unit test to use the repository mock.

Now we re-run the test and …

(env)[driti@ubuntu]$ python test_github.py
.
----------------------------------------------------------------------
Ran 1 test in 0.009s

OK

Wow, talk about a speed up! In fact, it was a 27x speed up off of a single test:

(env)[driti@ubuntu]$ python -c "print(0.245 / 0.009)"
27.2222222222

It’s important to note that we do not want to create our mocks within our test files, as this does not promote the idea of reusability. Thus, I highly recommend creating all your mocks within a mocks module to encourage reuse among existing and future tests.

The mock we created was quite basic. So let’s take a look at how we can further improve.

Tear the Roof Off

Thinking ahead, I’d like to add many more methods to my GitHub library to support the numerous functionalities of the GitHub API. However, I’m reluctant because this means I have to create many more mocks.

Immediately, I realize that my initial approach at designing a mock was rushed and has room for improvement. For starters, let’s approach the problem from a separation of concerns point of view. We have two concerns when it comes to mocking a service, the mock and the resource response. Lumping both these concerns into the same places makes my mock quite inflexible. So why don’t we try moving our resource response into a test fixture!

Lucky for us, GitHub followed the RESTful best practice of Addressability when designing their API. Addressability is defined by Leonard Richardson and Sam Ruby in RESTful Web Services as,

A web service is addressable if it exposes the interesting aspects of its data set through resources. Every resource has it’s own unique URI: in fact, URI just stands for “Universal Resource Identifier.”

Since every GitHub resource is uniquely addressed, this makes it easy for us to map a resource URI to a file on our file system. For example, the resource URI api.github.com/repos/appneta/burndown can just be a file containg JSON that is nested in several directories:

(env)[driti@ubuntu]$ mkdir -p api.github.com/repos/appneta
(env)[driti@ubuntu]$ echo '{"name":"burndown"}' > api.github.com/repos/appneta/burndown
(env)[driti@ubuntu]$ cat api.github.com/repos/appneta/burndown | python -m json.tool
{
    "name": "burndown"
}

Now let’s go ahead and update our mock and test:

  • (Commit) Refactor repository mock to use test fixtures.
(env)[driti@ubuntu]$ python test_github.py
.
----------------------------------------------------------------------
Ran 1 test in 0.009s

OK

Awesome!

Looking back at our repository mock, it actually contains nothing specific about the repository endpoint anymore (other then the path parameter). In fact, it seems like the mock can be reused for generic GET requests for any resource that has a test fixture on our file system.

The proof is in the pudding, so let’s update our mock:

  • (Commit) Rename repository mock to resource_get.

So let’s go ahead and put our updated resource_get mock to the test and see if it can properly handle requests for GitHub user information:

  • (Commit) Add get_user function, for getting GitHub user information.
  • (Commit) Add test_get_user test case, for testing our get_user function.
  • (Commit) Create a test fixture for user danriti.

Running our updated test suite yields sweet victory:

(env)[driti@ubuntu]$ python test_github.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.013s

OK

With our new and improved resource_get mock, we now only have to create test fixtures anytime we want to add new functionality and test it!

Whole Lot of Rhythm Going Round

Developers love refactoring, right? Well I can think of a few more changes that are appropriate.

First, our mock knows too much! Thus, I propose we do some good old information hiding and move the file handling functionality out of our mock and into a new class called Resource:

  • (Commit) Create Resource class for encapsulating file handling.

At first glance, you might think this is overengineering. While this may be true for simple file reading (i.e. mocking GET requests), you will have to deal with more complex file handling functionality when you start mocking POST, PUT, and DELETE requests. Thus, starting with this level of abstraction will help you in the long run.

Secondly, let’s introduce some error handling so our mock will respond properly with a 404 if a resource is not available:

  • (Commit) Add error handling if resource is not available.
(env)[driti@ubuntu]$ python test_github.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.011s

OK

Now I can rest easy, knowing that I have created a realistic, reusable and easy to maintain code base for mocking my dependency on GitHub!

Conclusion

Mocking external services can get complicated, fast. In this article, we’ve only covered the handling of GET requests (i.e. reading test fixtures), so it’s important to note that complexity may increase as you introduce mocking of POST, PUT, and DELETE requests.

However complex, I hope I have demonstrated the value to your test suite of using a structured and well organized approach when creating mocks for external services. Not only will your test suite improve in speed, but you’ll create a set of reusable mocks that can be leveraged by existing and future tests.

As a follow up, I highly encourage you to watch the PyCon 2014 talk by Augie Fackler and Nathaniel Manista titled Stop Mocking, Start Testing.

Summary
Article Name
Faking the Funk: Mocking External Services in Python Tests
Author
Description
We want to ensure our integration test suite is fast and consistent. Thus, let’s take a look at how we can leverage mocks to create a unit test environment in Python.
TwitterFacebookLinkedInRedditEmail

Slow Web Apps?

Web pages are complex. Download this free article to discover the four different ways you’re keeping your end users waiting.

Download the article