What You’ll Learn
In this lesson, you’ll learn how to use Python’s unittest.mock tools to test code that calls external HTTP APIs without making real network requests.
- Replace an HTTP request with a mock object.
- Configure simulated response data and exceptions.
- Verify the URL, parameters, and timeout passed to an API client.
- Use
side_effectto simulate failures or multiple responses. - Patch an object where your application looks it up.
The Concept
Unit tests should usually test one unit of application code in isolation. If that code calls an external API, making a real request during every test creates several problems:
- Tests become slower.
- They depend on network access.
- An external service can be unavailable or return different data.
- Tests might accidentally modify real data or consume API quota.
The unittest.mock module in Python’s standard library lets you replace a real object with a configurable mock. A mock can return a prepared response, raise an exception, record how it was called, and provide different results for successive calls.
The most common tools are:
MockandMagicMockfor creating fake objects.patchfor temporarily replacing an object during a test.calland assertion methods such asassert_called_once_withfor checking interactions.side_effectfor raising an exception or returning different values across calls.
A particularly important rule is to patch the name where the code under test looks it up. If api_client.py imports requests and calls requests.get, patch api_client.requests.get, not an unrelated name in your test file.
Basic Example
Suppose an application retrieves a user profile from an external service. The production code uses the third-party requests package, but the test replaces requests.get with a mock.
api_client.py
import requests
class ApiError(Exception):
pass
def fetch_profile(user_id):
url = f"https://profiles.example.test/users/{user_id}"
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
except requests.RequestException as exc:
raise ApiError("Profile service request failed") from exc
profile = response.json()
return {
"id": profile["id"],
"name": profile["name"],
}test_api_client.py
import unittest
from unittest.mock import Mock, patch
from api_client import ApiError, fetch_profile
class FetchProfileTests(unittest.TestCase):
@patch("api_client.requests.get")
def test_returns_selected_profile_fields(self, mock_get):
mock_response = Mock()
mock_response.json.return_value = {
"id": 42,
"name": "Amina Patel",
"internal_note": "not needed by this function",
}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
result = fetch_profile(42)
self.assertEqual(result, {"id": 42, "name": "Amina Patel"})
mock_get.assert_called_once_with(
"https://profiles.example.test/users/42",
timeout=5,
)
mock_response.raise_for_status.assert_called_once_with()
mock_response.json.assert_called_once_with()
@patch("api_client.requests.get")
def test_translates_request_failure(self, mock_get):
import requests
mock_get.side_effect = requests.Timeout("service timed out")
with self.assertRaises(ApiError):
fetch_profile(42)
if __name__ == "__main__":
unittest.main()Run the test module from the directory containing both files:
python -m unittest test_api_client.pyExpected Output
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OKHow the Code Works
@patch("api_client.requests.get") temporarily replaces the function used by fetch_profile. The real network function is restored automatically after the test finishes, even if the test fails.
mock_response = Mock() creates a fake response object. Its attributes can be configured as needed:
mock_response.json.return_valuecontrols whatresponse.json()returns.mock_response.raise_for_status.return_valuecontrols the behavior ofresponse.raise_for_status().mock_get.return_valuemakesrequests.get()return that fake response.
The first test checks both the returned data and the interaction with the API. The assertion on mock_get confirms that the code used the expected URL and a five-second timeout. This is useful because a test should verify important request behavior, not only the final result.
In the second test, mock_get.side_effect is set to a requests.Timeout exception. Calling fetch_profile then raises that exception as if the external service had timed out. The production function catches the library-specific exception and translates it into the application’s ApiError.
Notice that the patch target starts with api_client. That is where the production module looks up requests.get. Patching only a name in test_api_client.py would not replace the function used by fetch_profile.
Another Example
Mocks can also provide a sequence of responses. The following client follows a pagination link until the incident service has no more pages. The test uses side_effect with a list, so each call receives a different fake response.
incident_client.py
import requests
def list_incidents():
next_url = "https://status.example.test/api/incidents"
incidents = []
while next_url is not None:
response = requests.get(next_url, timeout=3)
response.raise_for_status()
page = response.json()
incidents.extend(page["incidents"])
next_url = page["next"]
return incidentstest_incident_client.py
import unittest
from unittest.mock import Mock, call, patch
from incident_client import list_incidents
class ListIncidentsTests(unittest.TestCase):
@patch("incident_client.requests.get")
def test_combines_all_pages(self, mock_get):
first_response = Mock()
first_response.json.return_value = {
"incidents": [
{"id": "inc-101", "summary": "Database latency"}
],
"next": "https://status.example.test/api/incidents?page=2",
}
second_response = Mock()
second_response.json.return_value = {
"incidents": [
{"id": "inc-102", "summary": "Queue backlog"}
],
"next": None,
}
mock_get.side_effect = [first_response, second_response]
result = list_incidents()
self.assertEqual(
result,
[
{"id": "inc-101", "summary": "Database latency"},
{"id": "inc-102", "summary": "Queue backlog"},
],
)
self.assertEqual(
mock_get.call_args_list,
[
call(
"https://status.example.test/api/incidents",
timeout=3,
),
call(
"https://status.example.test/api/incidents?page=2",
timeout=3,
),
],
)
if __name__ == "__main__":
unittest.main()This test is different from the profile example because it verifies a sequence of HTTP calls rather than a single call. It also checks that the client follows the URL returned by the first response instead of constructing the second URL incorrectly.
Common Mistakes
Patching the wrong location
Patch the name used by the module under test. For example, if incident_client.py calls requests.get, use @patch("incident_client.requests.get"). The module prefix is part of the target.
Forgetting to configure the mock response
A bare mock returns more mocks when its methods are called. If your code expects JSON data, configure mock_response.json.return_value. If it calls raise_for_status(), configure or verify that method too.
Testing only the returned value
A function could return the expected value while using the wrong URL, omitting authentication, or failing to set a timeout. Use assert_called_once_with or inspect call_args_list when request details are part of the function’s contract.
Over-specifying implementation details
Mock assertions should focus on behavior that matters. If the order of two internal helper calls is not important, avoid asserting every internal operation. Excessive assertions can make tests difficult to maintain during harmless refactoring.
Confusing a mock with an integration test
A mocked test verifies your code’s behavior around an API. It does not prove that the real API is available, that its authentication works, or that its response format has not changed. Those concerns belong in a small number of separately managed integration or contract tests.
Try It Yourself
Add a test for fetch_profile that verifies an HTTP error is translated into ApiError. Configure the fake response so that raise_for_status raises requests.HTTPError, then assert that fetch_profile(42) raises ApiError.
As an extra check, verify that the mock request still received the expected profile URL and timeout=5.
Challenge
Create a function named get_order_summary(order_id) in a module named order_client.py. It should:
- Request
https://orders.example.test/api/orders/{order_id}withtimeout=4. - Call
raise_for_status(). - Read the JSON response.
- Return a dictionary containing only
id,status, andtotal. - Translate any
requests.RequestExceptioninto anOrderServiceError.
Then write a unittest that mocks the request, checks the returned summary, and verifies the URL and timeout. Write a second test that simulates a connection error and checks the custom exception.
Solution
order_client.py
import requests
class OrderServiceError(Exception):
pass
def get_order_summary(order_id):
url = f"https://orders.example.test/api/orders/{order_id}"
try:
response = requests.get(url, timeout=4)
response.raise_for_status()
except requests.RequestException as exc:
raise OrderServiceError("Order service request failed") from exc
order = response.json()
return {
"id": order["id"],
"status": order["status"],
"total": order["total"],
}test_order_client.py
import unittest
from unittest.mock import Mock, patch
import requests
from order_client import OrderServiceError, get_order_summary
class GetOrderSummaryTests(unittest.TestCase):
@patch("order_client.requests.get")
def test_returns_order_summary(self, mock_get):
mock_response = Mock()
mock_response.json.return_value = {
"id": "order-7001",
"status": "paid",
"total": 149.95,
"customer_email": "customer@example.test",
}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
result = get_order_summary("order-7001")
self.assertEqual(
result,
{
"id": "order-7001",
"status": "paid",
"total": 149.95,
},
)
mock_get.assert_called_once_with(
"https://orders.example.test/api/orders/order-7001",
timeout=4,
)
mock_response.raise_for_status.assert_called_once_with()
mock_response.json.assert_called_once_with()
@patch("order_client.requests.get")
def test_translates_connection_error(self, mock_get):
mock_get.side_effect = requests.ConnectionError("connection refused")
with self.assertRaises(OrderServiceError):
get_order_summary("order-7001")
if __name__ == "__main__":
unittest.main()The solution patches order_client.requests.get, which is the lookup location used by get_order_summary. The successful test supplies extra response data to prove that the function returns only its documented summary fields. The failure test uses side_effect to simulate a connection error without contacting a real server.
Key Takeaways
- Use
patchto replace external HTTP calls during a unit test. - Patch the object where the module under test looks it up.
- Configure fake response methods with
return_value. - Use
side_effectto simulate exceptions or a sequence of API responses. - Verify important request details such as the URL, parameters, and timeout.



