← Home

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"] == 25

Critical 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 call

patch 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 automatically

Solution

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()

Resources