Module 6: Mocking, Fixtures and Validation Loops

Mocking Fundamentals

Mocking Fundamentals

Capsule overview

Your function calls the OpenAI API. Do you need to make a real request in every test? Your endpoint queries PostgreSQL. Do you need a database server running just to execute pytest? Your service generates files on disk. Do you want every test run to write and delete files on your system?

The answer is mocking: replacing external dependencies with controlled versions that simulate the real behavior. A mock of the OpenAI API returns a predefined response in microseconds. A mock of the database simulates queries with no disk. The tests become fast, deterministic and cheap.

This capsule teaches you the fundamentals of mocking in Python: unittest.mock (Mock, MagicMock, patch, return_value, side_effect) and pytest-mock. By the end, you'll be able to isolate code that depends on APIs, databases, the filesystem or time, and write tests that verify your logic's behavior without depending on the real world.


Why mocking exists

The problem: code that depends on the outside world

Your code usually depends on things outside your control:

  • External APIs: OpenAI, Stripe, Twitter, third-party services
  • Databases: PostgreSQL, Redis, MongoDB
  • The filesystem: reading/writing files, directories
  • Time: datetime.now(), time.sleep() — tests that depend on the clock are non-deterministic
  • Randomness: random.random() — every run gives different results

Without mocking, a test that calls the OpenAI API takes seconds, consumes credits, and can fail if the API is down or changes its response. A test that uses the real database requires PostgreSQL to be running, with known data, and it runs slower. The tests stop being a fast feedback loop and become a bottleneck.

The three principles of well-written tests

For tests to do their job, they need to be:

  1. Fast: running in milliseconds, not seconds. Thousands of tests in seconds.
  2. Deterministic: the same inputs → the same outputs. No surprises from the network, time or randomness.
  3. Independent: whether a test passes or fails shouldn't depend on the execution order or on other tests' state.

Mocking replaces external dependencies with controlled substitutes. You define exactly what each call returns. The tests stop depending on the real world and depend only on your logic.

Mocking as a controlled substitute

A mock is an object that simulates another one's behavior. Instead of calling the real API, you call an object that returns what you configured. That way you can:

  • Verify that your code called the API with the correct parameters
  • Simulate successful responses, errors, timeouts
  • Run hundreds of tests without a single real HTTP request

unittest.mock: Mock and MagicMock

Mock(): a generic mock object

Mock() creates an object that can act as anything. Any attribute you access exists automatically. Any method you call returns another Mock by default.

from unittest.mock import Mock

# Create a mock
mock_api = Mock()

# Configure what a method returns when it's called
mock_api.get_user.return_value = {"id": 1, "name": "Alice"}

# Call the method
result = mock_api.get_user(1)

# Verify the result
assert result["name"] == "Alice"

# Verify it was called correctly
mock_api.get_user.assert_called_once_with(1)

It doesn't matter what arguments you pass — the mock returns what you configured with return_value. You can pass get_user(999) or get_user() and it will still return {"id": 1, "name": "Alice"}. The assertions verify how the mock was used.

MagicMock(): a Mock with magic methods

MagicMock is like Mock but with Python's magic methods preconfigured: __len__, __iter__, __getitem__, etc. Use it when the code you're testing uses operators or built-ins that depend on these methods.

from unittest.mock import MagicMock

# MagicMock supports len(), iteration, indexing
mock_list = MagicMock()
mock_list.__len__.return_value = 3
assert len(mock_list) == 3

mock_dict = MagicMock()
mock_dict.__getitem__.return_value = "value"
assert mock_dict["key"] == "value"

In most cases, Mock is enough. Use MagicMock when you see errors like TypeError: object of type 'Mock' has no len().

When to use Mock vs MagicMock

SituationUse
An object that simulates an API, service or dependencyMock
An object used with len(), for x in obj, obj[key]MagicMock
An object compared with ==, in, operatorsMagicMock (it defines __eq__, __contains__, etc.)
You're not sureMagicMock — it's a superset, it rarely fails

MagicMock is more permissive: it responds to more operations without configuring anything. The cost is that it can hide bugs if your code uses an object incorrectly and the mock "works" anyway. For explicit dependencies (methods you call), Mock is usually enough and stricter.

return_value: what the call returns

return_value defines the value the mock returns when it's invoked as a function or method:

from unittest.mock import Mock

mock = Mock()
mock.calculate.return_value = 42

assert mock.calculate() == 42
assert mock.calculate(1, 2, 3) == 42  # Always 42, ignoring the args

side_effect: exceptions, multiple returns, custom functions

side_effect gives you fine control over the behavior:

from unittest.mock import Mock

# Raise an exception
mock = Mock()
mock.risky_call.side_effect = ConnectionError("timeout")
# mock.risky_call() → raises ConnectionError

# Return different values on successive calls
mock = Mock()
mock.fetch.side_effect = [{"id": 1}, {"id": 2}, {"id": 3}]
assert mock.fetch()["id"] == 1
assert mock.fetch()["id"] == 2
assert mock.fetch()["id"] == 3
# The 4th call raises StopIteration

# A custom function
mock = Mock()
mock.double.side_effect = lambda x: x * 2
assert mock.double(5) == 10

patch: replacing real objects temporarily

Creating mocks manually and passing them as parameters works when your code accepts dependencies by injection. But often the code imports and uses the object directly:

# mymodule.py
import requests

def fetch_data(url: str):
    response = requests.get(url)
    return response.json()

Here fetch_data uses requests.get directly. To mock it, you need to replace requests.get in the place where it's used (in mymodule), not where it's defined (in requests).

patch as a decorator

# tests/test_mymodule.py
from unittest.mock import patch
from mymodule import fetch_data


@patch("mymodule.requests.get")
def test_fetch_data(mock_get):
    # mock_get replaces requests.get inside mymodule
    mock_get.return_value.json.return_value = {"data": "test"}

    result = fetch_data("https://api.example.com")

    assert result == {"data": "test"}
    mock_get.assert_called_once_with("https://api.example.com")

The string "mymodule.requests.get" is the path where the object is used. If mymodule does from requests import get and then uses get, you patch "mymodule.get". The rule: patch where it's USED, not where it's DEFINED.

Where to patch: the golden rule

The most common confusion with patch is the path. The object is looked up in the namespace where it's used, not where it's defined. Examples:

The import in mymoduleThe code in mymoduleThe correct path for patch
import requestsrequests.get(url)"mymodule.requests.get"
from requests import getget(url)"mymodule.get"
import requests as reqreq.get(url)"mymodule.req.get"
from openai import OpenAIclient = OpenAI(); client.chat..."mymodule.OpenAI" (you patch the class)

If you patch "requests.get" instead of "mymodule.requests.get", the mymodule module already has a reference to the original requests.get in its namespace. Patching in requests doesn't affect that reference. That's why you patch where the name is bound in the module that makes the call.

patch as a context manager

from unittest.mock import patch
from mymodule import fetch_data


def test_fetch_data_with_context():
    with patch("mymodule.requests.get") as mock_get:
        mock_get.return_value.json.return_value = {"data": "test"}
        result = fetch_data("https://api.example.com")
    assert result == {"data": "test"}
    # Outside the with, requests.get is the original again

Useful when you want to apply the patch only in part of the test.

patch with pytest-mock: the mocker fixture

pytest-mock provides the mocker fixture, which simplifies using patch:

# tests/test_mymodule.py
def test_fetch_data(mocker):
    mock_get = mocker.patch("mymodule.requests.get")
    mock_get.return_value.json.return_value = {"status": "ok"}

    from mymodule import fetch_data
    result = fetch_data("https://api.example.com")

    assert result == {"status": "ok"}
    mock_get.assert_called_once_with("https://api.example.com")

With mocker, you don't need decorators or context managers. The fixture takes care of the patch's setup and teardown. Install it with pip install pytest-mock.

Where to patch: the golden rule

The most common cause of errors with patch is using the wrong path. The rule is: patch where the object is USED, not where it's DEFINED.

# api_client.py
import requests

def fetch_users():
    return requests.get("https://api.example.com/users").json()

Here requests is imported in api_client, and used as requests.get. The correct path is "api_client.requests.get" — the namespace where it's invoked.

If the module did this:

# api_client.py
from requests import get

def fetch_users():
    return get("https://api.example.com/users").json()

Then get lives in api_client's namespace. The correct path is "api_client.get", not "requests.get".

In summary: The patch string must be the complete path to the attribute as the code under test would see it when it runs. If mymodule does from foo import bar and uses bar(), you patch "mymodule.bar".


Assertions on mocks

Mocks record how they were used. You can verify that your code invoked them correctly.

Verification methods

from unittest.mock import Mock

mock = Mock()
mock.process(1, 2)
mock.process(3, 4)

# Was it called?
mock.process.assert_called()

# Was it called exactly once?
mock.process.assert_called_once()

# Was it called with these arguments?
mock.process.assert_called_with(1, 2)

# Was it called exactly once with these arguments?
mock.process.assert_called_once_with(1, 2)

# Was it not called?
mock.other_method.assert_not_called()

If the assertion fails, pytest shows a clear message: "Expected to be called once. Called 2 times."

Attributes for inspection

mock.process(1, 2)
mock.process(3, 4)

mock.process.call_count          # 2
mock.process.call_args           # call(3, 4) — the last call
mock.process.call_args_list      # [call(1, 2), call(3, 4)]
mock.process.call_args[0]        # (3, 4) — the args of the last call
mock.process.call_args[1]        # {} — the kwargs of the last call

side_effect for complex scenarios

Simulating an exception

from unittest.mock import Mock

mock_api = Mock()
mock_api.fetch.side_effect = ConnectionError("Network unreachable")

# Your code must handle the exception
# The test verifies that your code handles it correctly

Different returns on successive calls

mock = Mock()
mock.get_next.side_effect = [1, 2, 3, StopIteration]

assert mock.get_next() == 1
assert mock.get_next() == 2
assert mock.get_next() == 3
# The 4th call raises StopIteration

Useful when your code has a loop that calls the same method several times.

A custom function per call

mock = Mock()
def custom_logic(user_id):
    if user_id == 1:
        return {"name": "Admin"}
    return {"name": "User"}

mock.get_user.side_effect = custom_logic

assert mock.get_user(1)["name"] == "Admin"
assert mock.get_user(2)["name"] == "User"

pytest-mock: the mocker fixture

pytest-mock adds the mocker fixture to pytest. It's a wrapper over unittest.mock with cleaner syntax.

The advantages of mocker

  • You don't need @patch as a decorator
  • The patches are cleaned up automatically after the test
  • More concise syntax
  • Compatible with pytest's style (fixtures, parametrize)

A complete example

# src/ai_service.py
import openai

def generate_summary(text: str) -> str:
    response = openai.ChatCompletion.create(
        model="gpt-4",
        messages=[{"role": "user", "content": text}],
    )
    return response.choices[0].message["content"]
# tests/test_ai_service.py
def test_generate_summary(mocker):
    mock_create = mocker.patch("ai_service.openai.ChatCompletion.create")
    mock_create.return_value = {
        "choices": [{"message": {"content": "Short summary."}}]
    }

    from ai_service import generate_summary
    result = generate_summary("Long text to summarize.")

    assert result == "Short summary."
    mock_create.assert_called_once()
    call_kwargs = mock_create.call_args[1]
    assert call_kwargs["model"] == "gpt-4"
    assert "Long text" in call_kwargs["messages"][0]["content"]

When to mock and when not to

Over-mocking is a common mistake. Mocking the function you're testing means the test verifies nothing.

Mock

  • ✅ Calls to HTTP APIs (OpenAI, Stripe, etc.)
  • ✅ Database queries
  • ✅ Reading/writing on the filesystem
  • ✅ datetime.now(), time.sleep()
  • ✅ random.random(), uuid.uuid4()
  • ✅ Sending emails, messages to queues

Don't mock

  • ❌ The function you're testing
  • ❌ Pure logic (calculations, validations, transformations)
  • ❌ Simple data structures (lists, dictionaries)
  • ❌ Code that's already isolated and fast

Doubtful cases (judgment)

  • ⚠️ Internal modules of the same project: sometimes mock, sometimes don't — it depends on whether you want a unit test (isolated) or an integration test (with real dependencies).
  • ⚠️ Internal services that are slow: if an internal service takes 5 seconds, mocking can make sense in unit tests, but in integration tests you may want to use the real one.

A practical heuristic: if it's slow, non-deterministic, costly or has external side effects, consider it a candidate for mocking.

Comparison: tests with mocks vs without mocks

AspectWithout mocksWith mocks
SpeedSeconds per test (network, DB, I/O)Milliseconds per test
DeterminismDepends on external stateTotal control over inputs/outputs
Offline executionRequires services runningWorks without a network or DB
CostAPIs consume creditsZero cost
CI/CDFragile if services failStable and independent
IsolationAn external failure breaks many testsOnly the logic you're testing fails

Claude Code and mocking

Asking for mocks explicitly

When you ask Claude Code to generate tests for code with external dependencies, specify what to mock and how. A generic prompt can produce tests that call the real API. A specific prompt avoids that:

Generate unit tests for summarize_text() in summarizer.py.

Requirements:
- Mock httpx.post — there must be no real HTTP calls
- Use patch or mocker.patch with the path "summarizer.httpx.post"
- Verify that summarize_text returns the summary from the mocked response
- Include a test that verifies an empty text raises ValueError
- Use assert_called_once to verify the API was called

With that context, Claude Code generates tests that use mocks correctly.

Evaluating generated tests

When Claude Code gives you tests back, check:

  • Is the patch path correct? (where it's used, not where it's defined)
  • Is the external dependency being mocked and not the function under test?
  • Are there assertions that verify how the mock was used (assert_called_with, etc.)?
  • Do the tests pass without a network, a database or external services?

If a test takes several seconds or fails with "connection refused", it's probably calling the real service. Ask it to mock the dependency.

Iterating when the mock doesn't work

If the test fails with an "AttributeError" or the mock doesn't seem to apply:

  1. Check the path: Give Claude Code the module's content and ask: "What's the correct path to patch the API call in this module?"
  2. Import inside the test: If the module is imported at the top of the test file, the patch may be applied too late. Ask for the import of the function under test to be inside the test, after the patch.
  3. Give it the exact error: "The test fails with: [paste the traceback]. Fix the patch path."

Practice: mocking an AI service

Complete runnable code for you to practice with.

The module under test

# src/summarizer.py
import httpx

def summarize_text(text: str, api_url: str = "https://api.example.com/summarize") -> str:
    """Sends the text to an external API and returns the summary."""
    if not text or not text.strip():
        raise ValueError("Text cannot be empty")
    response = httpx.post(api_url, json={"text": text}, timeout=10.0)
    response.raise_for_status()
    return response.json()["summary"]

The project structure to practice with

project/
├── src/
│   └── summarizer.py
├── tests/
│   └── test_summarizer.py
└── pyproject.toml  # or requirements.txt with httpx

Make sure you have httpx installed (pip install httpx) and that the src directory is in Python's path. If you use the standard structure, run it from the root: pytest tests/ -v.

Tests with mocks

# tests/test_summarizer.py
import pytest
import httpx
from unittest.mock import Mock, patch


def test_summarize_returns_api_response():
    mock_response = Mock()
    mock_response.json.return_value = {"summary": "Short summary."}
    mock_response.raise_for_status = Mock()

    with patch("summarizer.httpx.post", return_value=mock_response) as mock_post:
        from summarizer import summarize_text
        result = summarize_text("Very long text that needs summarizing.")

    assert result == "Short summary."
    mock_post.assert_called_once()
    call_args = mock_post.call_args
    assert call_args[1]["json"]["text"] == "Very long text that needs summarizing."


def test_summarize_empty_text_raises():
    from summarizer import summarize_text

    with pytest.raises(ValueError, match="cannot be empty"):
        summarize_text("")


def test_summarize_http_error_propagates():
    """When the API returns an HTTP error, httpx.raise_for_status() raises."""
    mock_response = Mock()
    mock_response.status_code = 500
    mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
        "Server error", request=Mock(), response=mock_response
    )

    with patch("summarizer.httpx.post", return_value=mock_response):
        from summarizer import summarize_text

        with pytest.raises(httpx.HTTPStatusError):
            summarize_text("Valid text")

Exercises

Exercise 1: A basic mock with return_value (Easy)

Create a mock for an API client that has the method get_user(id: int). Configure the mock so it returns {"id": 42, "name": "Alice"}. Call the method and verify with assert that the name is "Alice". Use assert_called_once_with(42) to verify the call.

See solution
from unittest.mock import Mock

mock_client = Mock()
mock_client.get_user.return_value = {"id": 42, "name": "Alice"}

result = mock_client.get_user(42)

assert result["name"] == "Alice"
mock_client.get_user.assert_called_once_with(42)

Exercise 2: patch to mock requests (Medium)

You have this function that uses requests.get:

# weather.py
import requests

def get_temperature(city: str) -> float:
    r = requests.get(f"https://api.weather.com/{city}")
    return r.json()["temperature"]

Write a test that uses @patch to mock requests.get and verify that get_temperature("madrid") returns 22.5.

See solution
from unittest.mock import patch
from weather import get_temperature


@patch("weather.requests.get")
def test_get_temperature(mock_get):
    mock_get.return_value.json.return_value = {"temperature": 22.5}

    result = get_temperature("madrid")

    assert result == 22.5
    mock_get.assert_called_once_with("https://api.weather.com/madrid")

Note: You patch "weather.requests.get" because requests.get is used inside the weather module, not in the test module.

Exercise 3: side_effect for an exception (Medium)

A fetch_user(id: int) function calls api.get_user(id) and returns the user. If api.get_user raises ConnectionError, the function must return None. Write the test that verifies this behavior using a mock with side_effect.

See solution
from unittest.mock import Mock, patch

# Assuming user_service.py has:
# def fetch_user(id: int):
#     return api.get_user(id)  # returns None if ConnectionError

@patch("user_service.api")
def test_fetch_user_connection_error_returns_none(mock_api):
    mock_api.get_user.side_effect = ConnectionError("timeout")

    from user_service import fetch_user
    result = fetch_user(1)

    assert result is None

If fetch_user doesn't catch the exception yet, the test will fail until you implement the handling. That's the correct TDD flow.

Exercise 4: The mocker fixture with pytest-mock (Medium)

Refactor the test from Exercise 2 to use pytest-mock's mocker fixture instead of @patch. Keep the same verification.

See solution
def test_get_temperature(mocker):
    mock_get = mocker.patch("weather.requests.get")
    mock_get.return_value.json.return_value = {"temperature": 22.5}

    from weather import get_temperature
    result = get_temperature("madrid")

    assert result == 22.5
    mock_get.assert_called_once_with("https://api.weather.com/madrid")

It requires pip install pytest-mock.

Exercise 5: side_effect with multiple returns (Medium)

A mock of queue.pop() must return "first", then "second", and on the third call raise IndexError. Configure the mock and write a test that verifies the first two returns and that the third call raises the exception.

See solution
from unittest.mock import Mock
import pytest

mock_queue = Mock()
mock_queue.pop.side_effect = ["first", "second", IndexError("empty")]

assert mock_queue.pop() == "first"
assert mock_queue.pop() == "second"

with pytest.raises(IndexError, match="empty"):
    mock_queue.pop()

Note: IndexError("empty") as an element of the list makes the third call raise that exception. If you use IndexError (the class, not instantiated), side_effect will instantiate it when called.

Exercise 6: When to mock — a decision (Easy)

For each case, say whether you'd mock or not (and why):

  • a) A calculate_discount(price, percent) function that only does math operations
  • b) A send_notification(user_id) function that sends an email via SendGrid
  • c) A parse_json(text) function that uses json.loads from the stdlib
See solution
  • a) Don't mock. It's pure logic, fast and deterministic. Test it with real values.
  • b) Mock. SendGrid is an external API: slow, with a cost and side effects. Mock the call to SendGrid and verify that your code invokes it with the correct parameters.
  • c) Generally don't mock. json.loads is from the stdlib, fast and deterministic. Mocking it adds complexity with no benefit. The exception: if you want to simulate corrupt or malformed data that makes json.loads raise — in that case a side_effect with the exception could make sense, but normally passing invalid inputs is enough.

Troubleshooting

Problem 1: "AttributeError: module 'X' has no attribute 'Y'" when using patch

Cause: The patch path is incorrect. You're patching where the object is defined instead of where it's used.

Solution: If in mymodule.py you do import requests and then requests.get(url), the path is "mymodule.requests.get". If you do from requests import get and then get(url), the path is "mymodule.get". Always patch in the namespace of the module that makes the call.

Problem 2: The mock isn't used — the code keeps calling the real object

Cause: You import the module before applying the patch, or the patch is on the wrong path.

Solution: Apply the patch before the module loads the object. With decorators, the order is usually correct. With mocker.patch, make sure the patch is active before the import that runs the code. Sometimes you need to do the import inside the test, after the patch.

Problem 3: "assert_called_once_with" fails with "Expected call ... Actual call ..."

Cause: The actual arguments don't match the expected ones (order, types, values).

Solution: Check mock.call_args or mock.call_args_list to see exactly what was passed. Adjust your assert or fix the code under test if the arguments are wrong.

Problem 4: The mock returns another Mock in a chain (e.g. mock.a.b.c) and the test fails

Cause: You only configured return_value at the first level. mock.a returns a Mock, mock.a.b returns another Mock, etc. If your code expects a concrete value at mock.a.b.c, you need to configure it.

Solution: Configure the chain explicitly: mock.a.b.c.return_value = 42 or mock.a.return_value.b.c.return_value = 42. Or configure each level according to the structure your code expects.

Problem 5: side_effect with a list runs out and raises StopIteration

Cause: Your code makes more calls than you configured in the list.

Solution: Add more elements to the list or use a function in side_effect that handles any number of calls. If the number of calls is part of what you want to verify, an assert call_count == N can help.


Project Connection

This module's project is a Validation pipeline that depends on external services: the OpenAI API (or another AI API) and a database. You'll use mocks to:

  • Replace the calls to the AI API with predefined responses
  • Replace the database queries with a mock that returns controlled data

The fundamentals from this capsule (Mock, patch, return_value, side_effect, assertions) are the foundation of that project. Without mastering these concepts, you won't be able to isolate your logic from external services.

In the next capsule (Advanced fixtures) you'll learn to combine mocks with pytest fixtures to have a clean, reusable test setup.


Summary

  • Tests must be fast, deterministic and independent; mocking replaces external dependencies to achieve that.
  • Mock() and MagicMock() create objects that simulate behavior; return_value sets the return, side_effect allows exceptions, multiple returns or custom functions.
  • patch replaces objects temporarily; you must patch where the object is used, not where it's defined.
  • The assertions (assert_called_once_with, call_count, etc.) verify that the code used the mock correctly.
  • pytest-mock and the mocker fixture simplify using patch in pytest tests.
  • Mock external services (APIs, DB, filesystem, time); don't mock the function under test or pure logic.

Next capsule: Advanced pytest fixtures — scope, autouse, factories and conftest.py.


Additional Resources

  1. unittest.mock — Python Documentation — The official reference for Mock, MagicMock and patch
  2. pytest-mock — A pytest plugin for mocking
  3. Where to patch — A detailed explanation of patch paths
  4. Martin Fowler: Mocks Aren't Stubs — The differences between mocks, stubs and fakes
  5. Real Python: Python Mocking — A practical unittest.mock tutorial
  6. Google Testing Blog: Testing on the Toilet — Testing best practices (look for articles on mocking)

Module 6, Capsule 02 — Testing with Claude Code Guide