Mocking external APIs in Python

Published by at 26th January 2016 11:40 pm

It's quite common to have to integrate an external API into your web app for some of your functionality. However, it's a really bad idea to have requests be sent to the remote API when running your tests. At best, it means your tests may fail due to unexpected circumstances, such as a network outage. At worst, you could wind up making requests to paid services that will cost you money, or sending push notifications to clients. It's therefore a good idea to mock these requests in some way, but it can be fiddly.

In this post I'll show you several ways you can mock an external API so as to prevent requests being sent when running your test suite. I'm sure there are many others, but these have worked for me recently.

Mocking the client library

Nowadays many third-party services realise that providing developers with client libraries in a variety of languages is a good idea, so it's quite common to find a library for interfacing with a third-party service. Under these circumstances, the library itself is usually already thoroughly tested, so there's no point in you writing additional tests for that functionality. Instead, you can just mock the client library so that the request is never sent, and if you need a response, then you can specify one that will remain constant.

I recently had to integrate Stripe with a mobile app backend, and I used their client library. I needed to ensure that I got the right result back. In this case I only needed to use the Token object's create() method. I therefore created a new MockToken class that inherited from Token, and overrode its create() method so that it only accepted one card number and returned a hard-coded response for it:

1from stripe.resource import Token, convert_to_stripe_object
2from stripe.error import CardError
3
4
5class MockToken(Token):
6
7 @classmethod
8 def create(cls, api_key=None, idempotency_key=None,
9 stripe_account=None, **params):
10 if params['card']['number'] != '4242424242424242':
11 raise CardError('Invalid card number', None, 402)
12
13 response = {
14 "card": {
15 "address_city": None,
16 "address_country": None,
17 "address_line1": None,
18 "address_line1_check": None,
19 "address_line2": None,
20 "address_state": None,
21 "address_zip": None,
22 "address_zip_check": None,
23 "brand": "Visa",
24 "country": "US",
25 "cvc_check": "unchecked",
26 "dynamic_last4": None,
27 "exp_month": 12,
28 "exp_year": 2017,
29 "fingerprint": "49gS1c4YhLaGEQbj",
30 "funding": "credit",
31 "id": "card_17XXdZGzvyST06Z022EiG1zt",
32 "last4": "4242",
33 "metadata": {},
34 "name": None,
35 "object": "card",
36 "tokenization_method": None
37 },
38 "client_ip": "192.168.1.1",
39 "created": 1453817861,
40 "id": "tok_42XXdZGzvyST06Z0LA6h5gJp",
41 "livemode": False,
42 "object": "token",
43 "type": "card",
44 "used": False
45 }
46 return convert_to_stripe_object(response, api_key, stripe_account)

Much of this was lifted straight from the source code for the library. I then wrote a test for the payment endpoint and patched the Token class:

1class PaymentTest(TestCase):
2 @mock.patch('stripe.Token', MockToken)
3 def test_payments(self):
4 data = {
5 "number": '1111111111111111',
6 "exp_month": 12,
7 "exp_year": 2017,
8 "cvc": '123'
9 }
10 response = self.client.post(reverse('payments'), data=data, format='json')
11 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

This replaced stripe.Token with MockToken so that in this test, the response from the client library was always going to be the expected one.

If the response doesn't matter and all you need to do is be sure that the right method would have been called, this is easier. You can just mock the method in question using MagicMock and assert that it has been called afterwards, as in this example:

1class ReminderTest(TestCase):
2 def test_send_reminder(self):
3 # Mock PushService.create_message()
4 PushService.create_message = mock.MagicMock(name="create_message")
5
6 # Call reminder task
7 send_reminder()
8
9 # Check user would have received a push notification
10 PushService.create_message.assert_called_with([{'text': 'My push', 'conditions': ['UserID', 'EQ', 1]}])

Mocking lower-level requests

Sometimes, no client library is available, or it's not worth using one as you only have to make one or two requests. Under these circumstances, there are ways to mock the actual request to the external API. If you're using the requests module, then there's a responses module that's ideal for mocking the API request.

Suppose we have the following code:

1import json, requests
2
3def send_request_to_api(data):
4 # Put together the request
5 params = {
6 'auth': settings.AUTH_KEY,
7 'data': data
8 }
9 response = requests.post(settings.API_URL, data={'params': json.dumps(params)})
10 return response

Using responses we can mock the response from the server in our test:

1class APITest(TestCase):
2 @responses.activate
3 def test_send_request(self):
4 # Mock the API
5 responses.add(responses.POST,
6 settings.API_URL,
7 status=200,
8 content_type="application/json",
9 body='{"item_id": "12345678"}')
10
11 # Call function
12 data = {
13 "surname": "Smith",
14 "location": "London"
15 }
16 send_request_to_api(data)
17
18 # Check request went to correct URL
19 assert responses.calls[0].request.url == settings.API_URL

Note the use of the @responses.activate decorator. We use responses.add() to set up each URL we want to be able to mock, and pass through details of the response we want to return. We then make the request, and check that it was made as expected.

You can find more details of the responses module here.

Summary

I'm pretty certain that there are other ways you can mock an external API in Python, but these ones have worked for me recently. If you use another method, please feel free to share it in the comments.