C.W.K.
Stream
Lesson 02 of 05 · published

mock and unittest.mock — Faking the World

~18 min · mock, patch, monkeypatch, stub

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

Why mocking exists

Tests should be fast, deterministic, and isolated. A function that hits a real HTTP API, or reads a file, or queries a database, is none of those at test time — slow, flaky, full of side effects. Mocking lets you replace those external dependencies with stand-ins that return what you tell them to. The function being tested doesn't know the difference.

unittest.mock — Mock and patch

Mock() creates an object that records every call and accepts any attribute access. patch("module.path") replaces something in another module's namespace for the duration of the test (with-block, decorator, or context manager). The combination handles 90% of mocking needs.

The patch path — module-where-it's-used, not where-it's-defined

The most common mocking bug: patch("requests.get") when the code being tested does from requests import get; get(...). After import, the function in the test target's namespace is a separate reference; patching the original module doesn't affect it. The fix: patch("my_module.get") — patch where it's looked up, not where it's defined.

monkeypatch — pytest's lighter alternative

pytest's monkeypatch fixture provides similar capabilities with auto-cleanup at test end. monkeypatch.setattr(module, "attr", value) swaps something. monkeypatch.setenv("VAR", "value") sets an env var. Common pattern: the cleaner mock for pytest tests.

War Story: Over-mocking is a real anti-pattern. Tests that mock everything end up testing the mocks instead of the code. Pippa's stack mocks at boundaries — the network, the LLM API, the time function — but not internal calls between modules. The line: mock across processes and trust the code within.

Code

Mock — basics·python
from unittest.mock import Mock

# Create a mock — it accepts any call, returns a Mock
m = Mock()
m.do_something("foo", count=5)
m.other.deep.call()

# Inspect what happened
print(m.do_something.called)               # True
print(m.do_something.call_args)            # call('foo', count=5)
print(m.do_something.call_count)           # 1

# Set return values
m.compute.return_value = 42
print(m.compute())                          # 42
print(m.compute("any", "args"))            # 42 — args ignored

# Side effects — for raising or sequenced returns
m.flaky.side_effect = [1, 2, ValueError("oops")]
print(m.flaky())                           # 1
print(m.flaky())                           # 2
try:
    m.flaky()
except ValueError:
    print("raised on third")
patch — replacing in another module·python
# my_module.py
# import requests
# def fetch_user(uid):
#     resp = requests.get(f"https://api.example.com/{uid}")
#     return resp.json()

# test_my_module.py
from unittest.mock import patch, Mock

def test_fetch_user():
    fake_resp = Mock()
    fake_resp.json.return_value = {"name": "alice", "id": 42}

    # Patch where it's USED — my_module.requests, not just requests
    with patch("my_module.requests.get", return_value=fake_resp) as fake_get:
        # from my_module import fetch_user
        # result = fetch_user(42)
        # assert result == {"name": "alice", "id": 42}
        # fake_get.assert_called_once_with("https://api.example.com/42")
        pass
patch as decorator — cleaner for whole tests·python
from unittest.mock import patch, Mock

@patch("my_module.requests.get")
def test_with_decorator(mock_get):
    mock_get.return_value.json.return_value = {"x": 1}
    # ... call my_module.fetch ...
    # ... assert mock_get.called ...

# Multiple patches stack — innermost first in the parameter list
@patch("my_module.B")
@patch("my_module.A")
def test_stacked(mock_A, mock_B):
    # mock_A is the inner @patch, listed first
    # mock_B is the outer @patch, listed second
    pass
pytest's monkeypatch fixture·python
# Cleaner alternative to patch when you're already in pytest

def test_with_monkeypatch(monkeypatch):
    # Replace an attribute
    import my_module
    fake_get = lambda url: {"fake": True}
    monkeypatch.setattr(my_module, "fetch", fake_get)
    # ... test code ...
    # auto-cleaned at test end

    # Set env var (auto-restored)
    monkeypatch.setenv("DEBUG", "true")
    # ... test code ...

    # Change directory (auto-restored)
    monkeypatch.chdir("/tmp")
Asserting on calls·python
from unittest.mock import Mock

m = Mock()
m.send("hello", priority=1)
m.send("world", priority=5)

# Various assertions
m.send.assert_called()                  # at least once
m.send.assert_called_once()             # exactly once — fails if called twice
m.send.assert_called_with("world", priority=5)   # last call matched
m.send.assert_any_call("hello", priority=1)      # any call matched

# Inspect the full history
print(m.send.call_args_list)
# [call('hello', priority=1), call('world', priority=5)]

External links

Exercise

Write a function get_weather(city) in a module weather.py that imports requests and calls requests.get(url).json(). Write test_weather.py that uses patch("weather.requests.get") to mock the response — return a fake JSON dict. Verify (a) the test doesn't make a network call, (b) you can assert the URL passed to requests.get, (c) you can simulate a requests.RequestException via side_effect. Add a second test using monkeypatch instead of patch and compare.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.