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:
- Fast: running in milliseconds, not seconds. Thousands of tests in seconds.
- Deterministic: the same inputs → the same outputs. No surprises from the network, time or randomness.
- 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
| Situation | Use |
|---|---|
| An object that simulates an API, service or dependency | Mock |
An object used with len(), for x in obj, obj[key] | MagicMock |
An object compared with ==, in, operators | MagicMock (it defines __eq__, __contains__, etc.) |
| You're not sure | MagicMock — 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 mymodule | The code in mymodule | The correct path for patch |
|---|---|---|
import requests | requests.get(url) | "mymodule.requests.get" |
from requests import get | get(url) | "mymodule.get" |
import requests as req | req.get(url) | "mymodule.req.get" |
from openai import OpenAI | client = 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
@patchas 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
| Aspect | Without mocks | With mocks |
|---|---|---|
| Speed | Seconds per test (network, DB, I/O) | Milliseconds per test |
| Determinism | Depends on external state | Total control over inputs/outputs |
| Offline execution | Requires services running | Works without a network or DB |
| Cost | APIs consume credits | Zero cost |
| CI/CD | Fragile if services fail | Stable and independent |
| Isolation | An external failure breaks many tests | Only 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:
- 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?"
- 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.
- 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 usesjson.loadsfrom 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.loadsis 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 makesjson.loadsraise — in that case aside_effectwith 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()andMagicMock()create objects that simulate behavior;return_valuesets the return,side_effectallows exceptions, multiple returns or custom functions.patchreplaces 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-mockand themockerfixture 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
- unittest.mock — Python Documentation — The official reference for Mock, MagicMock and patch
- pytest-mock — A pytest plugin for mocking
- Where to patch — A detailed explanation of patch paths
- Martin Fowler: Mocks Aren't Stubs — The differences between mocks, stubs and fakes
- Real Python: Python Mocking — A practical unittest.mock tutorial
- Google Testing Blog: Testing on the Toilet — Testing best practices (look for articles on mocking)
Module 6, Capsule 02 — Testing with Claude Code Guide