Mocking external APIs in Python
Published by Matthew Daly 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_object2from stripe.error import CardError345class MockToken(Token):67 @classmethod8 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)1213 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": None37 },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": False45 }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")56 # Call reminder task7 send_reminder()89 # Check user would have received a push notification10 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, requests23def send_request_to_api(data):4 # Put together the request5 params = {6 'auth': settings.AUTH_KEY,7 'data': data8 }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.activate3 def test_send_request(self):4 # Mock the API5 responses.add(responses.POST,6 settings.API_URL,7 status=200,8 content_type="application/json",9 body='{"item_id": "12345678"}')1011 # Call function12 data = {13 "surname": "Smith",14 "location": "London"15 }16 send_request_to_api(data)1718 # Check request went to correct URL19 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.