Python testing - How to mock requests during tests
In the previous post, I wrote an introductory post about getting started with mocking tests in Python. You can find it here: Python testing - Introduction to mocking.
In this post, I'll discuss how you can mock HTTP requests made using urllib
or requests
package.
A simple example¶
This is a very simple example:
# main.py
import requests
def url_exists(url):
r = requests.get(url)
if r.status_code == 200:
return True
elif r.status_code == 404:
return False
The url_exists
function takes a url and makes an HTTP request to that url. If the
response status code is 200
, it returns True
, if status code is 404
, it
returns False
.
Now, we'll test if it works as expected. But instead of making actual requests to the url, we'll mock the requests and set the desired status codes for our mock object manually.
# tests.py
from unittest import TestCase
from mock import patch # for Python >= 3.3 use unittest.mock
from main import url_exists
class FetchTests(TestCase):
def test_returns_true_if_url_found(self):
with patch('requests.get') as mock_request:
url = 'http://google.com'
# set a `status_code` attribute on the mock object
# with value 200
mock_request.return_value.status_code = 200
self.assertTrue(url_exists(url))
def test_returns_false_if_url_not_found(self):
with patch('requests.get') as mock_request:
url = 'http://google.com/nonexistingurl'
# set a `status_code` attribute on the mock object
# with value 404
mock_request.return_value.status_code = 404
self.assertFalse(url_exists(url))
We can control the returned value of a mocked object using return_value
attribute. Above, we're setting status codes on mock_request.return_value
,
which means that mock_request.return_value
returns a "blank" object which
we can modify in any way we want, just like we've done above by setting a
status_code
attribute on it.
The reason we've done it is because requests.get
returns a response object,
if we mock it, mock_request
should also return a response object. But, as
mentioned above, it returns a "blank" object, not an actual response object.
So, we set a status_code
attribute on it to mimic a response.
If all this is confusing, you can create a separate class for a fake response. See below.
Let's run the tests:
$ python -m unittest tests
# Output
..
----------------------------------------------------------------------
Ran 2 test in 0.003s
OK
So it worked!
Testing if the mocked object actually gets called¶
In our tests above, we only test the return value of url_exists
function.
But there's a problem with our tests. We don't know for sure if the url_exists
function calls requests.get
method because we've mocked it in our tests.
We have to know for sure that url_exists
function really calls requests.get
method.
Well, mock
makes it easy for us to test that. To test if url_exits
tries
to call requests.get
once with the given url
, we can use assert_called_once_with
attribute on the mocked object.
NOTE: From our small example, it is very obvious that url_exists
does call
requests.get
method. But in a large piece of code, with a lot of variables and
conditional statements, things don't remain so obvious any more - you will have
to write tests to make sure things work as expected.
Let's modify our test:
class FetchTests(TestCase):
def test_returns_true_if_url_found(self):
with patch('requests.get') as mock_request:
url = 'http://google.com'
# set a `status_code` attribute on the mock object
# with value 200
mock_request.return_value.status_code = 200
self.assertTrue(url_exists(url))
# test if requests.get was called
# with the given url or not
mock_request.assert_called_once_with(url)
def test_returns_false_if_url_not_found(self):
with patch('requests.get') as mock_request:
url = 'http://google.com/nonexistingurl'
# set a `status_code` attribute on the mock object
# with value 404
mock_request.return_value.status_code = 404
self.assertFalse(url_exists(url))
# test if requests.get was called
# with the given url or not
mock_request.assert_called_once_with(url)
Run it again and it should still work.
Faking the response¶
At certain times, just mocking requests isn't enough because you'd need to do some operations on the response. For those purposes, you can make the mocked request object return a fake response for the purpose of testing.
Let's write another function in the main.py file called process_response
.
# main.py
import requests
def process_response(url):
r = requests.get(url)
return r.content
The process_response
function just requests a url and returns the content of
the response. It's a very simple example.
Since mock
allows you to set attributes on the mocked object on-the-fly while
testing, setting a fake response is pretty straight forward. We can even create
attributes of attributes. So, to mock the content, we'll need to set a content
attribute on the the return_value
of the mock_request
, something like -
mock_request.return_value.content = "Fake content"
.
It's very convenient that we can create attributes of attributes like this. In a general Python class, you'll first need to create an attribute before you can create it's sub-attribute.
Let's test our code:
# tests.py
from unittest import TestCase
from mock import patch
from main import process_response
class ProcessResponseTests(TestCase):
def test_response_content_is_not_empty(self):
with patch('requests.get') as mock_request:
url = 'http://google.com'
# set fake content
mock_request.return_value.content = "Fake content"
response = process_response(url)
self.assertNotNone(response.content)
Run the tests again and they should all pass.
Creating a dedicated class for fake response¶
In this post, I've used mock_request.return_value
to set status code, content,
etc on the fake response. This can get repetitive and there's a better way to
deal with fake responses.
You can create a separate class for the fake response where you'd set default values for the response. That should reduce some of the repetitive code. It would work like this:
class FakeResponse(object):
# default response attributes
status_code = 200
content = "Some content"
# then use it like this:
fake_response = FakeResponse()
mock_request.return_value = fake_response
# change the default values if you want
fake_response.status_code = 404
fake_response.content = "Hello, world"
Conclusion¶
In this post we saw how we can mock HTTP requests by mocking requests.get
or
(urllib.urlopen
). We also saw that we can mock requests.get
directly or we
can mock the funciton/code that calls requests.get
. Both the ways work, so it
doesn't matter which way we choose.