Kartoza - Mocking Requests with requests_mock

Testing is an important part of software development, be it manual or automated.

 · 4 min read

Testing is an important part of software development, be it manual or automated. Untested code is a ticking time bomb ready to explode at the worst time possible, hence each developer must be responsible for testing their code. It’s even better when they make automated tests for their code, even if it’s only unit tests.

There could be a time when we need to develop a feature relying on a 3rd party service. Let’s say we have a GIS app for farms and plan on a new feature where the user could subscribe to weather information. The weather data is retrieved from a paid 3rd party service and we are in charge of developing the feature.

When the user subscribes to our new feature, our app will create a subscription to the weather service. We created create_weather_subscription in a file named create_weather_subscription.py that will return the response of the service, which will be used by another function to save the subscription ID or inform the user if there is an error. Below is a simplified example of the subscribe function.

import requests

def create_weather_subscription(user_id, package_id, start_date, end_date):
    payload = {
        'user_id': user_id,
        'package_id': package_id,
        'start_date': start_date,
        'end_date': end_date,
    }
    response = requests.post(
        'https://real-weather-service.com/weather/subscribe/', data=payload
    )
    return response

How can we test that function? Using the real service to subscribe to weather data is definitely not an option. Imagine running tests numerous times and creating at least 1 subscription for each test. That would cost us a lot. Mocking create_weather_subscription is also not a good idea since there could be errors in our function, and mocking it could make the test give false positive results.

Using requests-mock

One way to test this code is by mocking our request to the weather service using requests-mock. With requests-mock, we can preload the requests with responses that are returned if certain URIs are requested. This is particularly useful in unit tests where we want to return known responses from HTTP requests without making actual calls. To ensure the test result, we need to correctly set the response data and status code for each test.

First, install requests-mock using pip install requests-mock. Then, create a unit test for that function in a file named test_create_weather_subscription.py.

import requests_mock
import unittest

from create_weather_subscription import create_weather_subscription

class TestSubscribeWeather(unittest.TestCase):
"""
Test create_weather_subscription function
"""

@classmethod
def setUpClass(cls):
cls.user_id = 10
cls.package_id = 'dummy-package-id'
cls.start_date = '2020-10-01'
cls.end_date = '2020-12-31'

def test_create_weather_subscription_success(self):
with requests_mock.Mocker() as rm:
response = create_weather_subscription(
self.user_id, self.package_id, self.start_date, self.end_date
)

self.assertEqual(response, 'Weather data subscribed successfully!')


if __name__ == '__main__':
unittest.main()

We will now run the test, and see what happens.

$ python test_create_weather_subscription.py
... error traceback ...
requests_mock.exceptions.NoMockAddress: No mock address: POST https://real-weather-service.com/weather/subscribe/
.
----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (errors=1)

Uh oh! Why do we get that? This is actually a nice feature of requests-mock where it prevents us from accessing real URI. Remember that we need to preload the response before it is actually executed? We haven't done that. Try mocking up the requests before we call the subscribe function, noting that it has to be inside the with statement.

return_value = {
id': 101,
"user_id": self.user_id,
"package_id": self.package_id,
"start_date": self.start_date,
"end_date": self.end_date,
}
rm.post('https://real-weather-service.com/weather/subscribe/', json=return_value, status_code=201)

A json parameter is used to define the return value from the service, while status_code is used to define the status code of the requests. Other parameters are also available, you can go ahead to the documentation to find out.

Now run the test.

$ python test_create_weather_subscription.py
.
----------------------------------------------------------------------
Ran 1 test in 0.002s

OK

Yay! We finally do one test case where the susbcription succeeds. We should also add test cases where the susbcription fails. Now, our test will look like this:

import requests_mock
import unittest

from create_weather_subscription import create_weather_subscription

class TestSubscribeWeather(unittest.TestCase):
"""
Test create_weather_subscription function
"""

@classmethod
def setUpClass(cls):
cls.user_id = 10
cls.package_id = 'dummy-package-id'
cls.start_date = '2020-10-01'
cls.end_date = '2020-12-31'

def test_create_weather_subscription_success(self):
"""
Simply test when subscription is created successfully.
"""
return_value = {
'id': 101,
"user_id": self.user_id,
"package_id": self.package_id,
"start_date": self.start_date,
"end_date": self.end_date,
}
with requests_mock.Mocker() as rm:
rm.post('https://real-weather-service.com/weather/subscribe/', json=return_value, status_code=201)
response = create_weather_subscription(
self.user_id, self.package_id, self.start_date, self.end_date
)

self.assertEqual(response, 'Weather data subscribed successfully!')

def test_create_weather_subscription_authorization_error(self):
"""
Test when subscription failed because of authorization error.
"""
return_value = {'message': 'Unauthorized'}
with requests_mock.Mocker() as rm:
rm.post('https://real-weather-service.com/weather/subscribe/', json=return_value, status_code=401)
response = create_weather_subscription(
self.user_id, self.package_id, self.start_date, self.end_date
)

self.assertEqual(response, 'Unauthorized')

def test_create_weather_subscription_overlap(self):
"""
Test when subscription failed because there is overlapping subscription for
the dates specified for the user.
"""
return_value = {'message': 'New subscription overlaps existing subscription'}
with requests_mock.Mocker() as rm:
rm.post('https://real-weather-service.com/weather/subscribe/', json=return_value, status_code=400)
response = create_weather_subscription(
self.user_id, self.package_id, self.start_date, self.end_date
)

self.assertEqual(response, 'New subscription overlaps existing subscription')


if __name__ == '__main__':
unittest.main()

And finally, our tests run without a problem.

$ python test_create_weather_subscription.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.003s

OK

Easy, right? Now try for yourself, and share your experience using requests-mock in the comments section.


Zulfikar Akbar Muzakki

Zakki is a software developers from Indonesia and is based in Purworejo, a small town in Central Java. He studied Information System in Universitas Indonesia. His journey with Python and Django started when he first worked as a web developer. After discovering the simplicity and power of Python he has stuck to it. You know what people say, “Once you go Python, you’ll never move on!”. His interest in GIS stemmed from his activities exploring random things in the digital map. He looks for cities, interesting places, even following street view from one place to another, imagining he was there physically. Zakki is a volunteer in kids learning centre, where he and his team run fun activities every Sunday. When he is not dating his computer, he spends his time sleeping or watching documentary and fantasy movies. Give him more free time, and he will ride his motorbike to someplace far away. He enjoys watching cultural shows and learning about people, places, and cultures. He also loves singing and dancing, but that doesn’t mean he is good at it.

No comments yet.

Add a comment
Ctrl+Enter to add comment