Unit tests are easier to write when given a set of inputs you know you're always going to get an expected response. It becomes a bit difficult when the same set of inputs can lead to varying responses.
When you work on programs where sometimes you have to make a request to an external API or making requests to other services in your microservices, you can't be sure you're going to get 200
status code on each request.
You'll mostly put in measures to handle when your requests fail but how do you write unit tests to ensure your system works as expected.
Writing Program
Let's build a program that a pings a url and returns True
if the status code is 200
and False
when it is not.
- Create a file called
client.py
- Write the following code
# client.py
# Make sure you have requests installed.
# If not, you can install with `pip3 install requests`
# or `pip install requests`.
import requests
def ping(url):
res = requests.get(url)
retun res.status_code == 200
print(ping("https://google.com"))
- Run
python3 client.py
in your terminal in the directory whereclient.py
resides. The below should be your response;
❯ python3 client.py
True
Writing Tests
Our program works as expected. It's time to write tests to ensure that we return True
when our request is 200
and False
when the status code is not 200
.
- Create a file called
test_client.py
in the same directory asclient.py
- Write the following code;
# test_client.py
import unittest
from client import ping
class TestClient(unittest.TestCase):
def setUp(self):
self.url = "https://google.com"
def test_ping_returns_200(self):
result = ping(self.url)
self.assertTrue(result)
if __name__ == "__main__":
unittest.main()
- Run
python3 test_client.py
. You should get a response like below;
❯ python3 test_client.py
.
---------------------------------------------------------
Ran 1 test in 1.113s
OK
All our tests are passing but now let's test for the case when the status code is not 200
. Let's add another test to our test
# test_client.py
import unittest
from client import ping
class TestClient(unittest.TestCase):
def setUp(self):
self.url = "https://google.com"
def test_ping_returns_200(self):
result = ping(self.url)
self.assertTrue(result)
# New
def test_ping_returns_500(self):
result = ping(self.url)
self.assertFalse(result)
if __name__ == "__main__":
unittest.main()
When we rerun python3 test_client.py
, our new test fails.
❯ python3 test_client.py
.F
==========================================================
FAIL: test_ping_returns_500 (__main__.TestClient.test_ping_returns_500)
----------------------------------------------------------
Traceback (most recent call last):
File "/Users/mock/test_client.py", line 46, in test_ping_returns_500
self.assertFalse(result)
AssertionError: True is not false
-----------------------------------------------------------
Ran 2 tests in 2.076s
FAILED (failures=1)
This is because for now our request returns 200
, so we are unable to test for when our ping
fails.
To be able to do this we are going to mock our requests and responses. This gives us the ability to determine what response we should get.
This way we can manipulate our requests to return 500
response status code instead of the actual status code.
Mocking requests and responses
First we're going to import the patch
decorator and MagicMock
object from unittest.mock
.
The patch
decorator allows us to edit our requests
, it takes a target which will be the requests we want to change, which in this case is the one in client.py
.
The MagicMock
object allows us to create the response we want so we can pass it to requests.
We have add an additional parameter which can be named anyway we want. I'll name it mock_requests
. This is the object we can attach our response to.
Let's add these changes to our test_client.py
;
# test_client.py
import unittest
from unittest.mock import patch, MagicMock
from client import ping
class TestClient(unittest.TestCase):
def setUp(self):
self.url = "https://google.com"
def test_ping_returns_200(self):
result = ping(self.url)
self.assertTrue(result)
@patch("client.requests") # New
def test_ping_returns_500(self, mock_requests): # New
# New
mock_response = MagicMock()
mock_response.status_code = 500
mock_requests.get.return_value = mock_response
result = ping(self.url)
self.assertFalse(result)
if __name__ == "__main__":
unittest.main()
Let's dive into what we've done so far.
- By adding the decorator
@patch("client.requests")
, we're saying we want to modify therequests
function that we imported inclient.py
. It can be any function, we could have targetedping
itself. You can try that on your own after this. Thepatch
decorator modifies our test method and passes in a second argument to ourtest_ping_returns_500
method. Thus, we addmock_requests
as a parameter to our method. Thismock_requests
is a mock variant ofrequests
. - We instantiate our
MagicMock
object and assign tomock_response
. OurMagicMock
object allows us to take the form of any class we want and in this case aResponse
object. - We add a
status_code
attribute to ourmock_response
and assign it the value of500
. - We proceed to assign our
mock_response
tomock_requests.get.return_value
. We have now modified our requests in that, we're saying, when therequests
inclient.py
calls it'sget
function, we want the value returned to be that of ourmock_response
. Thus whenrequests.get()
is called, the value returned is going to be themock_response
.
If we run our test again, we see everything passes now.
❯ python3 test_client.py
..
----------------------------------------------------------------------
Ran 2 tests in 1.609s
OK
We can proceed to do the same for test_ping_returns_200
method but make the status_code 200
so we can add a fake url instead of making requests to https://google.com
.
# test_client.py
import unittest
from unittest.mock import patch, MagicMock
from client import ping
class TestClient(unittest.TestCase):
def setUp(self):
self.url = "https://api.service.com" # New
@patch("client.requests") # New
def test_ping_returns_200(self, mock_requests): # New
# New
mock_response = MagicMock()
mock_response.status_code = 200
mock_requests.get.return_value = mock_response
result = ping(self.url)
self.assertTrue(result)
@patch("client.requests")
def test_ping_returns_500(self, mock_requests):
mock_response = MagicMock()
mock_response.status_code = 500
mock_requests.get.return_value = mock_response
result = ping(self.url)
self.assertFalse(result)
if __name__ == "__main__":
unittest.main()
We can run our tests again to ensure everything works well.
❯ python3 test_client.py
..
----------------------------------------------------------------------
Ran 2 tests in 1.609s
OK
Mocking response body
We can also add response body to our mock_response
. Assuming, the 3rd party API we're pinging returns a json response with an attribute {"status": "ok"}
.
- Let's modify our function in
client.py
to return the json response too when the status_code is200
andNone
when it is not.
# client.py
import requests
def ping(url):
res = requests.get(url)
# New
if res.status_code == 200:
return (True, res.json())
return (False, None)
- We modify our
test_client.py
to this;
# test_client.py
import unittest
from unittest.mock import patch, MagicMock
from client import ping
class TestClient(unittest.TestCase):
def setUp(self):
self.url = "https://api.service.com"
@patch("client.requests")
def test_ping_returns_200(self, mock_requests):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"status": "ok"} # New
mock_requests.get.return_value = mock_response
status, body = ping(self.url) # New
self.assertTrue(status)
self.assertEqual(body["status"], "ok")
@patch("client.requests")
def test_ping_returns_500(self, mock_requests):
mock_response = MagicMock()
mock_response.status_code = 500
mock_requests.get.return_value = mock_response
status, body = ping(self.url) # New
self.assertFalse(status)
if __name__ == "__main__":
unittest.main()
Adding mock_response.json.return_value
, we're saying when we call the json
function on our mock_response
we should get {"status": "ok"}
.
Thus, {"status": "ok"}
is the value we get when we call res.json()
in client.py
.
Conclusion
Mocking requests make testing easier especially when you have to deal with 3rd party APIs and even other services in your microservices.
Let's say, you have an API that first makes a request to your auth service to confirm if a user has certain permissions before allowing them access to certain resources.
You can mock the response you have to test that only users with the needed permissions are allowed to access the resource.
Code for this tutorial can be found here https://github.com/quamejnr/python-mock-requests
Top comments (0)