Mocking in Software Tests

This week I learned about mocking code behavior when writing tests. This is really cool, so I thought I would write a little about it.

Recently, I wrote a Python wrapper for the Open Brewery DB API. At first, the tests I wrote for my Python wrapper included code that actually requested data from the Open Brewery DB external server. This wasn’t ideal because the tests took a long time to run (a few seconds for each test, but this can add up during development when you’re running tests frequently). In addition, if the API server went down for any period of time, then the tests for the Python wrapper would start to fail, even if there were no changes made to the code.

This post walks through writing a test for a function that makes an HTTP request, while avoiding actually making the request.

Example HTTP request

Below is a get_org_repo_data function that uses the Requests library to make an HTTP request to the GitHub API to get information about a GitHub organization’s repositories.

import requests

def get_org_repo_data(org):
    url = f'{org}/repos'
    r = requests.get(url)
    if r.status_code == 200:
        return r.json()
        return None

The function returns either JSON data about the organization’s repositories, or None if the request response status_code indicates there was an issue with the request.

When writing tests for get_org_repo_data, we’ll want to make sure that None is returned when the status_code for the request response is not 200. This could be done, for instance, by passing get_org_repo_data the name of a non-existent GitHub organization.

from example import get_org_repo_data

def test_get_org_repo_data_invalid():
    result = get_org_repo_data(org='this-is-an-invalid-org')
    assert result is None

Because the organization 'this-is-an-invalid-org' doesn’t exist on GitHub, the request response status_code will be 404 and get_org_repo_data will return None. This test will pass. However, every time the test is run, it will make an HTTP request to the GitHub API, which introduces some of the problems mentioned at the beginning of this post.

Is there a better way?

Mocking requests.get

Mocking is a way to replace the normal functionality of some piece of code. The unittest.mock library in the Python standard library has a Mock class and patch function decorator that will help us do this (note that unittest.mock was added in Python version 3.3).

Instances of the Mock class are callable and allow you to assign return values and class attributes as you wish.

from unittest.mock import Mock

a = Mock(status_code=200)
assert a.status_code == 200
a.return_value = 'foo'
assert a() == 'foo'

Mock objects are super flexible, you can give them any attributes you want. The patch function decorator can be used to replace a specified function with a mock object. So setting the return_value for the mock object allows you to modify the patched function to behave however you want it to.

In the test_get_org_repo_data_invalid test, we can use Mock and patch together to have the requests.get function return a mock object with a status_code attribute that indicates an error. The updated test that mocks requests.get looks like:

from example import get_org_repo_data
from unittest.mock import Mock, patch

def test_get_org_repo_data_invalid(mock_get):
    mock_get.return_value = Mock(status_code=404)
    result = get_org_repo_data(org='this-is-an-invalid-org')
    assert result is None

Here we used the patch decorator to replace the requests.get function in with a mock object that we’ve called mock_get (note that the mock object gets passed as an extra argument to the decorated test function). We then assign the return value of mock_get to be another mock object with a status_code attribute of 404. Now when test_get_org_repo_data_invalid is run we are still testing that None is returned, but we did so without actually making an HTTP request!

Here we used the unittest.mock library to mock an HTTP request. However, Mock and patch can be used to mock any component of a codebase! This is a useful technique to have in your testing toolbox.