115. Unittest.Mock Module
Mocking dependencies for isolated unit testing
115. Unittest.Mock Module
🧪 A testing suite mocks HTTP API calls to test without hitting the network. The developer forgot to set return_value on their mock (so it returns a MagicMock object instead of real data), and patched the wrong import path (must patch where the name is USED, not where it is defined).
💡 Fun fact: The unittest.mock module was added to Python’s standard library in Python 3.3 (2012), though it existed as a third-party library (mock) for years before that. MagicMock is an enhanced version of Mock that automatically implements Python’s “magic methods” (__len__, __iter__, __str__, etc.), so a MagicMock can be used anywhere Python expects an object that supports len(), iteration, or string conversion. Every attribute access on a MagicMock returns another MagicMock — this is intentional and makes it easy to mock deeply nested objects.
⚠️ Watch out: The most common mock mistake is patching the wrong path. If weather_client.py does import requests, you must patch weather_client.requests.get, NOT requests.get. The @patch decorator replaces the name in the module where it is used, not in the module where it was originally defined. Patching the wrong location means the real function still runs during the test — usually causing a network call, a database write, or some other side effect you were trying to avoid.
🤔 Think about it: A MagicMock() with no return_value set returns another MagicMock when called. This means mock_response.json() returns a MagicMock, not a dict — and mock_response.json()["temperature"] may silently return yet another MagicMock instead of raising an error. Your test might pass even though no real data was returned. How would you write a test that explicitly verifies the mock returned real data and not a nested MagicMock?
Learning objectives
- Set return_value on a MagicMock so it returns real data, not another MagicMock
- Patch the name where it is USED (mymodule.requests), not where it is defined
- Use @patch decorator or the with patch() context manager for clean setup/teardown
- Assert on mock.call_count, mock.call_args to verify your code called dependencies correctly
- Use MagicMock() to build fake response objects with controlled attributes and methods
Key concepts
- MagicMock() — flexible fake object
- mock.return_value — what the mock returns when called
- mock.method.return_value — what mock.method() returns
- @patch(‘module.name’) — replace a name during the test
- Patch where used, not where defined
Try it
Concept detail
unittest.mock — Replace Dependencies in Tests
Mocking replaces real dependencies (APIs, databases, files) with controlled fake objects during testing.
MagicMock Basics
from unittest.mock import MagicMock
mock = MagicMock()
mock.status_code = 200 # Set attribute
mock.json.return_value = {"k": "v"} # Set method return value
print(mock.status_code) # 200
print(mock.json()) # {"k": "v"}patch() Decorator
from unittest.mock import patch
# Patches 'mymodule.requests.get' for the duration of the test
@patch("mymodule.requests.get")
def test_fetch(mock_get):
mock_get.return_value = MagicMock(status_code=200)
mock_get.return_value.json.return_value = {"temp": 25}
result = fetch_weather("Mumbai")
assert result["temp"] == 25Critical Rule: Patch WHERE It’s Used
# weather_client.py does: import requests
# WRONG — patches the real requests module, not your copy:
@patch("requests.get")
# RIGHT — patches the reference in your module:
@patch("weather_client.requests.get")Verifying Mock Calls
mock_get.assert_called_once()
mock_get.assert_called_with("https://api.example.com/data")
print(mock_get.call_count) # Number of times called
print(mock_get.call_args) # Arguments of last callpatch as Context Manager
with patch("mymodule.requests.get") as mock_get:
mock_get.return_value = fake_response
result = fetch_weather("Delhi")
# After the with block, original is restored automaticallySolution
import json
class _FakeRequests:
"""Simulated requests library for Pyodide environment."""
class Response:
def __init__(self, data, status_code=200):
self._data = data
self.status_code = status_code
def json(self):
return self._data
@staticmethod
def get(url, timeout=5):
raise RuntimeError("Real network call — should be mocked in tests!")
requests = _FakeRequests()
def fetch_weather(city: str) -> dict:
"""Fetch weather data for a city. Returns dict with temp and condition."""
response = requests.get(f"https://api.weather.example.com/{city}", timeout=5)
if response.status_code != 200:
return {"error": "API request failed", "city": city}
data = response.json()
return {
"city": city,
"temp": data.get("temperature"),
"condition": data.get("condition"),
}
def get_temperature(city: str) -> float:
"""Return just the temperature for a city."""
weather = fetch_weather(city)
if "error" in weather:
return -999.0
return weather["temp"]Tests
from unittest.mock import MagicMock, patch
def make_mock_response(temperature=22.5, condition="Sunny", status_code=200):
mock_resp = MagicMock()
mock_resp.status_code = status_code
mock_resp.json.return_value = {
"temperature": temperature,
"condition": condition,
}
return mock_resp
def test_fetch_weather_with_mock():
"""Directly replace requests.get on the module-level object."""
original_get = requests.get
mock_get = MagicMock(return_value=make_mock_response(25.0, "Cloudy"))
requests.get = mock_get
try:
result = fetch_weather("Mumbai")
assert result["city"] == "Mumbai"
assert result["temp"] == 25.0
assert result["condition"] == "Cloudy"
finally:
requests.get = original_get
def test_mock_was_called_with_city():
"""Verify the mock was called with the right URL."""
original_get = requests.get
mock_get = MagicMock(return_value=make_mock_response(18.0, "Rainy"))
requests.get = mock_get
try:
fetch_weather("Delhi")
call_url = mock_get.call_args[0][0]
assert "Delhi" in call_url, f"Expected 'Delhi' in URL, got: {call_url}"
finally:
requests.get = original_get
def test_get_temperature_returns_float():
original_get = requests.get
mock_get = MagicMock(return_value=make_mock_response(30.0, "Hot"))
requests.get = mock_get
try:
temp = get_temperature("Chennai")
assert temp == 30.0
assert isinstance(temp, float)
finally:
requests.get = original_get
def test_fetch_weather_api_error_returns_error_dict():
"""When API returns non-200, fetch_weather should return error dict."""
original_get = requests.get
mock_get = MagicMock(return_value=make_mock_response(status_code=503))
requests.get = mock_get
try:
result = fetch_weather("Nowhere")
assert "error" in result
assert result["city"] == "Nowhere"
finally:
requests.get = original_get
def test_get_temperature_returns_sentinel_on_error():
"""get_temperature returns -999.0 when API fails."""
original_get = requests.get
mock_get = MagicMock(return_value=make_mock_response(status_code=404))
requests.get = mock_get
try:
temp = get_temperature("GhostCity")
assert temp == -999.0
finally:
requests.get = original_get
def test_mock_return_value_is_not_magic_mock():
"""Ensure mock response has real data, not a nested MagicMock."""
mock_resp = make_mock_response(15.0, "Windy")
# If return_value wasn't set on .json, calling .json() returns a MagicMock
data = mock_resp.json()
assert isinstance(data, dict), (
"mock.json() returned MagicMock — set return_value on mock_resp.json"
)
assert data["temperature"] == 15.0
def test_no_real_network_call_without_mock():
"""Without a mock, fetch_weather should raise (proves mock isolation works)."""
try:
fetch_weather("RealCity")
# If we get here, the mock wasn't in place — this is a test design issue
# but we accept it in Pyodide context where _FakeRequests raises
pass
except RuntimeError as e:
assert "mocked" in str(e).lower() or "network" in str(e).lower()