89

I am writing an application that performs REST operations using Kenneth Reitz's requests library and I'm struggling to find a nice way to unit test these applications, because requests provides its methods via module-level methods.

What I want is the ability to synthesize the conversation between the two sides; provide a series of request assertions and responses.

zipzapzoop45
  • 39
  • 1
  • 1
  • 10
Chris R
  • 17,546
  • 23
  • 105
  • 172

7 Answers7

65

It is in fact a little strange that the library has a blank page about end-user unit testing, while targeting user-friendliness and ease of use. There's however an easy-to-use library by Dropbox, unsurprisingly called responses. Here is its intro post. It says they've failed to employ httpretty, while stating no reason of the fail, and written a library with similar API.

import unittest

import requests
import responses


class TestCase(unittest.TestCase):

  @responses.activate  
  def testExample(self):
    responses.add(**{
      'method'         : responses.GET,
      'url'            : 'http://example.com/api/123',
      'body'           : '{"error": "reason"}',
      'status'         : 404,
      'content_type'   : 'application/json',
      'adding_headers' : {'X-Foo': 'Bar'}
    })

    response = requests.get('http://example.com/api/123')

    self.assertEqual({'error': 'reason'}, response.json())
    self.assertEqual(404, response.status_code)
saaj
  • 23,253
  • 3
  • 104
  • 105
  • Updated URL for the `responses` [**intro post**](http://cra.mr/2014/05/20/mocking-requests-with-responses) – Mohamed Ragab Feb 21 '18 at 10:19
  • Interestingly, since this was posted as an answer, David Cramer –who authored this library– moved on and founded Sentry, and [moved the library with him](https://github.com/getsentry/responses/commit/fe1e389dabea0a79cba59b720ef6d91ff40f53cb). This is why on GitHub it’s under the `getsentry` org. – bfontaine Aug 18 '19 at 17:50
50

If you use specifically requests try httmock. It's wonderfully simple and elegant:

from httmock import urlmatch, HTTMock
import requests

# define matcher:
@urlmatch(netloc=r'(.*\.)?google\.com$')
def google_mock(url, request):
    return 'Feeling lucky, punk?'

# open context to patch
with HTTMock(google_mock):
    # call requests
    r = requests.get('http://google.com/')
print r.content  # 'Feeling lucky, punk?'

If you want something more generic (e.g. to mock any library making http calls) go for httpretty.

Almost as elegant:

import requests
import httpretty

@httpretty.activate
def test_one():
    # define your patch:
    httpretty.register_uri(httpretty.GET, "http://yipit.com/",
                        body="Find the best daily deals")
    # use!
    response = requests.get('http://yipit.com')
    assert response.text == "Find the best daily deals"

HTTPretty is far more feature-rich - it offers also mocking status code, streaming responses, rotating responses, dynamic responses (with a callback).

Chen Levy
  • 15,438
  • 17
  • 74
  • 92
Marek Brzóska
  • 861
  • 7
  • 7
22

You could use a mocking library such as Mocker to intercept the calls to the requests library and return specified results.

As a very simple example, consider this class which uses the requests library:

class MyReq(object):
    def doSomething(self):
        r = requests.get('https://api.github.com', auth=('user', 'pass'))
        return r.headers['content-type']

Here's a unit test that intercepts the call to requests.get and returns a specified result for testing:

import unittest
import requests
import myreq

from mocker import Mocker, MockerTestCase

class MyReqTests(MockerTestCase):
    def testSomething(self):
        # Create a mock result for the requests.get call
        result = self.mocker.mock()
        result.headers
        self.mocker.result({'content-type': 'mytest/pass'})

        # Use mocker to intercept the call to requests.get
        myget = self.mocker.replace("requests.get")
        myget('https://api.github.com', auth=('user', 'pass'))
        self.mocker.result(result)

        self.mocker.replay()

        # Now execute my code
        r = myreq.MyReq()
        v = r.doSomething()

        # and verify the results
        self.assertEqual(v, 'mytest/pass')
        self.mocker.verify()

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

When I run this unit test I get the following result:

.
----------------------------------------------------------------------
Ran 1 test in 0.004s

OK
srgerg
  • 18,719
  • 4
  • 57
  • 39
3

Missing from these answers is requests-mock.

From their page:

>>> import requests
>>> import requests_mock

As a context manager:

>>> with requests_mock.mock() as m:

...     m.get('http://test.com', text='data')
...     requests.get('http://test.com').text
...
'data'

Or as a decorator:

>>> @requests_mock.mock()
... def test_func(m):
...     m.get('http://test.com', text='data')
...     return requests.get('http://test.com').text
...
>>> test_func()
'data'
Community
  • 1
  • 1
Unapiedra
  • 15,037
  • 12
  • 64
  • 93
  • Do you have any knowledge of how to make this work with ``pytest``? I tried the exact example you're citing. Ref.: https://stackoverflow.com/questions/47703748/why-does-the-simplest-requests-mock-example-fail-with-pytest – Thomas Fauskanger Dec 08 '17 at 00:59
  • If I remember correctly, I used the decorator. And I think this also worked with pytest. – Unapiedra Dec 18 '17 at 16:08
  • I made it work with decorators, but it appears (on my system) that it's in conflict with some other argument, so I had to pass a kw argument to the Mocker, as mentioned in [docs](https://requests-mock.readthedocs.io/en/latest/mocker.html#decorator). Not sure if this has to do with pytest, but the error that came up mentioned fixtures. Thanks for getting back to the issue. – Thomas Fauskanger Dec 18 '17 at 17:37
2

using mocker like in srgerg's answer:

def replacer(method, endpoint, json_string):
    from mocker import Mocker, ANY, CONTAINS
    mocker = Mocker()
    result = mocker.mock()
    result.json()
    mocker.count(1, None)
    mocker.result(json_string)
    replacement = mocker.replace("requests." + method)
    replacement(CONTAINS(endpoint), params=ANY)
    self.mocker.result(result)
    self.mocker.replay()

For the requests library, this would intercept the request by method and endpoint you're hitting and replace the .json() on the response with the json_string passed in.

jchysk
  • 1,538
  • 1
  • 15
  • 27
1

If you break out your response handler/parser into a separate function, you can work with requests.Response objects directly, without needing to mock the client-server interaction.

Code under test

from xml.dom import minidom
from requests.models import Response

def function_under_test(s3_response: Response):
    doc = minidom.parseString(s3_response.text)

    return (
        s3_response.status_code,
        doc.getElementsByTagName('Code').item(0).firstChild.data,
    )

Test code

import unittest
from io import BytesIO

class Test(unittest.TestCase):

    def test_it(self):
        s3_response = Response()
        s3_response.status_code = 404
        s3_response.raw = BytesIO(b"""<?xml version="1.0" encoding="UTF-8"?>
            <Error>
            <Code>NoSuchKey</Code>
            <Message>The resource you requested does not exist</Message>
            <Resource>/mybucket/myfoto.jpg</Resource> 
            <RequestId>4442587FB7D0A2F9</RequestId>
            </Error>
        """)

        parsed_response = function_under_test(s3_response)

        self.assertEqual(404, parsed_response[0])
        self.assertEqual("NoSuchKey", parsed_response[1])
Trenton
  • 11,678
  • 10
  • 56
  • 60
0

There's a library for this, if you want to write your test server with Flask: requests-flask-adaptor

You just have to be careful with the order of imports when monkeypatching.

falsePockets
  • 3,826
  • 4
  • 18
  • 37