Skip to main content

🎭 Mocking and Patching in Python Tests

Real code talks to APIs, databases, files, and the clock — dependencies that are slow, flaky, or impossible to control from a test. Mocking replaces those dependencies with stand-ins you fully command, so you can isolate the code under test. This lesson covers Python's unittest.mock end to end: building mocks, the make-or-break rule of where to patch, verifying calls, and the anti-patterns that quietly ruin a suite.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what mocking is and choose between Mock, MagicMock, and autospec
  • Configure mocks with return values and side effects (exceptions, sequences, functions)
  • Apply the rule "patch where an object is used, not where it's defined"
  • Verify interactions with mock assertions like assert_called_once_with
  • Mock HTTP, files, and time, and recognize mocking anti-patterns

Estimated Time: 50–60 minutes  •  Difficulty: Intermediate

Hands-on: Mock every external dependency of a weather-service client.

In This Lesson

What Is Mocking?

A mock is a fake object that stands in for a real one during a test. You tell it what to return and what to raise, and afterward you can ask it how it was called. Mocking lets you isolate the unit under test from the messy outside world — a payment gateway, a REST API, a database, the filesystem, or the system clock.

💡 Analogy: Mocking is a flight simulator for your code. Instead of risking a real aircraft (a live API, real money, a production database), you run in a controlled environment that mimics real conditions — including the failures you could never trigger on demand, like a timeout or a 500 error.
mindmap root((Mocking)) What Fake objects Controlled responses Recorded calls Why Isolate the unit Simulate failures Faster tests When External APIs Databases Files Time & randomness

The unittest.mock Library

Mocking is in the standard library (unittest.mock, since Python 3.3) — no install required. Its main pieces:

ToolWhat it does
MockA flexible fake; every attribute/method access auto-creates another Mock
MagicMockLike Mock but also supports magic methods (len(), [], with, +)
patchTemporarily replaces a target object for the duration of a test
AsyncMockA mock whose calls are awaitable, for async code
call / ANYHelpers to describe and match calls in assertions
from unittest.mock import Mock, MagicMock

# A plain Mock records how it's used
mock = Mock()
mock.method("hi")
assert mock.method.called

# MagicMock supports magic methods a plain Mock does not
magic = MagicMock()
len(magic)          # works: returns 0 by default
magic[0]            # works: returns another MagicMock
with magic:         # works: supports the context-manager protocol
    pass

📖 Mock vs MagicMock

Use Mock for ordinary objects and functions. Reach for MagicMock when the code under test uses dunder (magic) methods on the dependency — indexing, iteration, len(), arithmetic, or the with statement. When in doubt, MagicMock is the safe default.

Configuring Mocks

Return values

from unittest.mock import Mock

mock = Mock(return_value=42)
assert mock() == 42

# Configure a method's return value
mock.get_user.return_value = {"id": 1, "name": "Ray"}
assert mock.get_user()["name"] == "Ray"

Side effects

side_effect unlocks richer behavior. Set it to an exception to raise, a list to return successive values, or a function to compute the result:

from unittest.mock import Mock

# 1. Raise an exception
mock = Mock(side_effect=ValueError("boom"))
# mock() now raises ValueError("boom")

# 2. Return a different value on each call
mock = Mock(side_effect=[1, 2, 3])
assert [mock(), mock(), mock()] == [1, 2, 3]

# 3. Delegate to a function
def double_or_reject(x):
    if x < 0:
        raise ValueError("negative")
    return x * 2

mock = Mock(side_effect=double_or_reject)
assert mock(5) == 10

spec and autospec — safer mocks

A bare Mock accepts any attribute, so a typo like mock.grret() silently passes. A spec constrains the mock to the real object's interface; autospec goes further and also checks the call signatures:

from unittest.mock import Mock, create_autospec

class Person:
    def greet(self, other):
        return f"Hi {other}"

# spec: only real attributes are allowed
mock = Mock(spec=Person)
mock.greet("Ana")     # OK
# mock.dance()        # AttributeError — Person has no `dance`

# autospec: also enforces the signature
auto = create_autospec(Person)
# auto.greet()        # TypeError — missing the `other` argument

✅ Prefer autospec for public interfaces

Autospec catches the sneakiest failure mode in mocking: your test keeps passing after you rename or change the signature of the real method, because a bare mock happily accepts the old call. With autospec, the mock changes when the real API changes.

Patching & Where to Patch

patch swaps out a target for a mock, then restores the original when the test ends. It works as a decorator, a context manager, or manually:

# app.py
import requests

def get_user(user_id):
    resp = requests.get(f"https://api.example.com/users/{user_id}")
    return resp.json() if resp.status_code == 200 else None
# test_app.py
from unittest.mock import patch, Mock
from app import get_user

@patch("app.requests.get")            # decorator form
def test_get_user(mock_get):
    mock_get.return_value = Mock(status_code=200,
                                 json=Mock(return_value={"id": 1, "name": "Ray"}))
    assert get_user(1) == {"id": 1, "name": "Ray"}
    mock_get.assert_called_once_with("https://api.example.com/users/1")

def test_as_context_manager():
    with patch("app.requests.get") as mock_get:      # context-manager form
        mock_get.return_value = Mock(status_code=404)
        assert get_user(999) is None
flowchart LR A[Original object] --> B[patch replaces it with a Mock] B --> C[Test body runs] C --> D[patch restores the original]

The golden rule: patch where it's used

This trips up nearly everyone. You must patch the name in the module that uses it — not the module where it was originally defined. Because from x import y creates a new reference y inside the importing module, patching the original x.y leaves that reference untouched.

⚠️ The most common mocking mistake

# services.py
import requests
def fetch(url):
    return requests.get(url).json()

# consumer.py
from services import fetch          # `fetch` now lives in consumer's namespace
def process(user_id):
    return fetch(f"https://api.example.com/users/{user_id}")
# WRONG — patches the original, not the reference consumer.py uses
@patch("services.fetch")
def test_wrong(mock_fetch): ...     # process() still calls the real fetch!

# RIGHT — patch the name where it is looked up
@patch("consumer.fetch")
def test_right(mock_fetch):
    mock_fetch.return_value = {"name": "Ray"}
    assert process(1) == {"name": "Ray"}

The tell: if you did from services import fetch in consumer.py, patch consumer.fetch. If instead you did import services and call services.fetch(), patch services.fetch. Patch the name at the location where the code looks it up.

Stacked patches and patch.object

# Decorators apply bottom-up, so arguments read inside-out
@patch("app.logging")               # -> outer arg
@patch("app.requests.get")          # -> inner arg (closest to the function)
def test_two_patches(mock_get, mock_logging):
    ...

# Patch a single attribute/method of a class
from unittest.mock import patch
@patch.object(UserService, "get_user", return_value={"name": "Ray"})
def test_object_patch(mock_get_user):
    ...

# Temporarily set environment variables
@patch.dict("os.environ", {"API_KEY": "test-key"})
def test_env(): ...

Mock Assertions

Every mock records its calls, so you can verify your code interacted with its dependencies correctly:

from unittest.mock import Mock, call, ANY

mock = Mock()
mock(1, 2, key="value")

mock.assert_called()                       # called at least once
mock.assert_called_once()                  # called exactly once
mock.assert_called_with(1, 2, key="value") # most recent call matched these args
mock.assert_called_once_with(1, 2, key="value")

# Never called?
other = Mock()
other.assert_not_called()

# Inspect call history
mock(3)
assert mock.call_count == 2
assert mock.call_args_list == [call(1, 2, key="value"), call(3)]

# Don't care about some args? Use ANY
mock.assert_called_with(ANY, ANY, key="value")

For a sequence of method calls, assert_has_calls checks that they occurred (in order by default):

service = Mock()
service.process(1)
service.process(2)
service.finish("batch1")

service.assert_has_calls([
    call.process(1),
    call.process(2),
    call.finish("batch1"),
])

⚠️ Beware the typo trap

A bare mock will happily accept a misspelled assertion like mock.asssert_called() and do nothing — the test passes without checking anything. Autospec'd mocks reject unknown attributes, so combining autospec with real assertions is the safest habit.

Common Scenarios

HTTP requests

import requests
from unittest.mock import patch, Mock

def get_user_data(user_id):
    resp = requests.get(f"https://api.example.com/users/{user_id}")
    return resp.json() if resp.status_code == 200 else None

@patch("app.requests.get")
def test_get_user_data(mock_get):
    mock_get.return_value = Mock(status_code=200,
                                 json=Mock(return_value={"id": 1, "name": "Ray"}))
    assert get_user_data(1) == {"id": 1, "name": "Ray"}

    mock_get.return_value = Mock(status_code=404)
    assert get_user_data(999) is None

💡 For HTTP, consider the responses library

Manually mocking requests gets verbose. The third-party responses library lets you register fake endpoints declaratively (responses.add(responses.GET, url, json=..., status=200)), which reads more clearly for API-heavy code.

File operations with mock_open

from unittest.mock import patch, mock_open

def read_first_line(path):
    with open(path) as f:
        return f.readline().strip()

@patch("builtins.open", new_callable=mock_open, read_data="hello\nworld")
def test_read_first_line(mock_file):
    assert read_first_line("data.txt") == "hello"
    mock_file.assert_called_once_with("data.txt")

Time and randomness

Time-dependent code is untestable until you control the clock. Patch time.time with a side_effect list to script successive readings:

from unittest.mock import patch

def elapsed(func):
    import time
    start = time.time()
    func()
    return time.time() - start

@patch("app.time.time")
def test_elapsed(mock_time):
    mock_time.side_effect = [1000.0, 1030.0]   # start, then end
    assert elapsed(lambda: None) == 30.0

Async code with AsyncMock

import pytest
from unittest.mock import AsyncMock, patch

@patch("app.fetch_data", new_callable=AsyncMock)
async def test_async(mock_fetch):
    mock_fetch.return_value = {"data": "test"}
    result = await process("https://example.com/api")
    mock_fetch.assert_awaited_once_with("https://example.com/api")

Anti-Patterns

Mocking is sharp — used carelessly it produces tests that pass while the real code is broken. Watch for these:

Anti-patternWhy it hurts
Over-mocking — mocking every collaboratorThe test only exercises the interaction of your mocks, not real behavior
Mocking the system under testPatching the method you're testing means you test the mock, not the code
Asserting implementation detailsTests break on harmless refactors even though outputs are still correct
Bare mocks without specTypos and stale signatures pass silently; use autospec
# Anti-pattern: mocking what you're trying to test
@patch("module.MyClass.method_under_test")
def test_useless(mock_method):
    mock_method.return_value = "expected"
    assert MyClass().method_under_test() == "expected"   # tests nothing real

# Anti-pattern: over-asserting the implementation
@patch("module.helper")
def test_brittle(mock_helper):
    process_data([1, 2, 3])
    mock_helper.assert_called_with([1, 2, 3])   # breaks on any refactor

# Better: assert the outcome
def test_outcome():
    assert process_data([1, 2, 3]) == [2, 4, 6]

⚠️ A cautionary tale

Over-mocking can hide real bugs: a suite passes because the mocks were told to return the expected values, while the actual code path — the one that runs in production — is broken. Mock the boundaries of your system (network, disk, clock), and let your own logic run for real.

An alternative: dependency injection

Often the cleanest way to make code testable is to pass in its dependencies instead of hard-coding them. Then a test simply supplies a fake — no patching needed:

# Hard to test: dependency is created inside
def process():
    client = ApiClient()
    return client.get_data()

# Easy to test: dependency is injected
def process(client=None):
    client = client or ApiClient()
    return client.get_data()

def test_process():
    fake = Mock()
    fake.get_data.return_value = "data"
    assert process(client=fake) == "data"

Hands-on Exercise

🏋️ Mock a weather service

Objective: Test a client that calls an external API — without ever hitting the network. Here is the code:

# weather.py
import requests

class WeatherService:
    def __init__(self, api_key):
        if not api_key:
            raise ValueError("API key is required")
        self.api_key = api_key
        self.base_url = "https://api.weatherapi.com/v1"

    def current(self, city):
        resp = requests.get(f"{self.base_url}/current.json",
                            params={"key": self.api_key, "q": city})
        if resp.status_code != 200:
            return {"error": f"Failed: {resp.status_code}"}
        data = resp.json()
        return {"city": data["location"]["name"],
                "temp_c": data["current"]["temp_c"]}

Your tasks

  1. Patch weather.requests.get and test a successful current("London") call returns the parsed dict.
  2. Assert requests.get was called with the correct URL and params.
  3. Test the error path: a non-200 status returns the {"error": ...} dict.
  4. Verify the constructor raises ValueError when the API key is empty.
💡 Hint

Remember to patch where it's used: @patch("weather.requests.get"). Build the fake response with Mock(status_code=200, json=Mock(return_value={...})). Check the call args with mock_get.assert_called_once_with(url, params={...}).

✅ Sample solution
# test_weather.py
import pytest
from unittest.mock import patch, Mock
from weather import WeatherService

SAMPLE = {"location": {"name": "London"}, "current": {"temp_c": 12.0}}

@patch("weather.requests.get")
def test_current_success(mock_get):
    mock_get.return_value = Mock(status_code=200,
                                 json=Mock(return_value=SAMPLE))
    service = WeatherService("key123")

    result = service.current("London")

    assert result == {"city": "London", "temp_c": 12.0}
    mock_get.assert_called_once_with(
        "https://api.weatherapi.com/v1/current.json",
        params={"key": "key123", "q": "London"},
    )

@patch("weather.requests.get")
def test_current_error(mock_get):
    mock_get.return_value = Mock(status_code=500)
    service = WeatherService("key123")
    assert service.current("London") == {"error": "Failed: 500"}

def test_requires_api_key():
    with pytest.raises(ValueError, match="API key is required"):
        WeatherService("")

Notice the network is never touched — the tests are fast, deterministic, and can even exercise the 500 path you could never reliably trigger against the real API.

Best Practices

✅ Do🚫 Avoid
Mock the boundaries: network, disk, clock, randomnessMocking your own logic that you're trying to verify
Patch where the name is usedPatching where it was originally defined
Use autospec/spec to keep mocks honestBare mocks that accept any attribute or signature
Assert on outcomes; verify calls only when the interaction is the contractPinning down every internal helper call
Prefer dependency injection when it makes code cleanerReaching for patch to work around poor design
💡 The goal is isolation, not avoidance. Mock to remove uncontrollable dependencies so your logic can be tested — not to sidestep testing real behavior. If a test is nothing but mocks talking to mocks, it isn't testing your code.

✅ pytest-mock makes this tidier

Under pytest, the pytest-mock plugin provides a mocker fixture: mock_get = mocker.patch("app.requests.get"). It wraps unittest.mock and undoes every patch automatically at the end of the test — no decorators or with blocks to manage.

Summary & Quiz

🎉 Key Takeaways

  • A mock is a controllable stand-in that also records how it was called; MagicMock adds magic-method support.
  • Configure behavior with return_value and side_effect; use spec/autospec to keep mocks faithful to the real interface.
  • Patch where an object is used, not where it's defined — the single most common mocking mistake.
  • Verify interactions with assert_called_once_with, call_args_list, and ANY.
  • Mock the boundaries (HTTP, files, time, async) and avoid over-mocking, which hides real bugs.

🎯 Quick Quiz

Question 1: In consumer.py you wrote from services import fetch. Which target should you patch?

Question 2: You need a mock to return 1, then 2, then 3 on successive calls. What do you set?

Question 3: Why is over-mocking dangerous?

📚 Further Reading

🚀 What's Next?

You've now covered testing across the Python ecosystem. Next we cross into the PHP world and apply the same testing mindset — assertions, isolation, and structure — in a new language with PHPUnit Testing Framework.

🎉 Nice work!

You can now isolate any unit of code from the messy world around it — and prove it talks to that world correctly.