pytest-mock provides a mocker fixture wrapping unittest.mock. pip install pytest-mock. mocker.patch("module.function") — replaces function for the test scope. mocker.patch.object(obj, "method") — patches a method on a specific object. mocker.patch("path.Class", autospec=True) — spec-respecting mock. mocker.spy(obj, "method") — wraps real method, tracks calls. mocker.stub(name="stub") — empty callable. mock = mocker.MagicMock(). mock.return_value = 42. mock.side_effect = ValueError("error"). mock.side_effect = [1, 2, 3] — returns each in sequence. Assertions: mock.assert_called_once(), mock.assert_called_once_with(arg1, kwarg=val), mock.assert_called_with(...) (last call), mock.assert_not_called(). Call inspection: mock.call_count, mock.call_args, mock.call_args_list, mock.called. call_args_list[0].args, call_args_list[0].kwargs. Reset: mocker.resetall() — clears call records. mocker.stopall() — stops all patches. AsyncMock: mocker.patch("module.async_fn", return_value=data) — auto-detects async; or mocker.AsyncMock(). Context manager mock: mocker.patch("module.open", mock_open(read_data="content")). Property mock: mocker.patch.object(obj, "prop", new_callable=PropertyMock, return_value=42). Class attribute: mocker.patch.object(MyClass, "CLASS_VAR", 99). mocker.patch.dict(os.environ, {"KEY": "val"}). mocker.patch("builtins.open", mocker.mock_open(...)). Claude Code generates pytest-mock fixtures, spy wrappers, and async mock patterns for API client testing.
CLAUDE.md for pytest-mock
## pytest-mock Stack
- Version: pytest-mock >= 3.14 | pip install pytest-mock
- Fixture: mocker — available in all test functions automatically
- Patch: mocker.patch("pkg.module.name") | mocker.patch.object(obj, "attr")
- Autospec: mocker.patch("Class", autospec=True) — preserves signature
- Spy: mocker.spy(obj, "method") — wraps real impl, tracks calls
- Async: mocker.patch auto-detects async; or mocker.AsyncMock()
- Verify: assert_called_once_with | call_args_list for multi-call inspection
pytest-mock Mocking Pipeline
# tests/test_mock_pipeline.py — pytest-mock patterns for common scenarios
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, PropertyMock, call, mock_open
import pytest
import requests
# ── Code under test ───────────────────────────────────────────────────────────
class EmailService:
def __init__(self, api_key: str, sender: str):
self.api_key = api_key
self.sender = sender
def send(self, to: str, subject: str, body: str) -> dict:
resp = requests.post(
"https://api.email.com/send",
headers={"Authorization": f"Bearer {self.api_key}"},
json={"from": self.sender, "to": to, "subject": subject, "body": body},
)
resp.raise_for_status()
return resp.json()
class UserRepository:
def __init__(self, db):
self.db = db
def find_by_email(self, email: str) -> dict | None:
row = self.db.execute(
"SELECT id, name, email FROM users WHERE email = ?", (email,)
).fetchone()
if row is None:
return None
return {"id": row[0], "name": row[1], "email": row[2]}
def create(self, name: str, email: str) -> dict:
cursor = self.db.execute(
"INSERT INTO users (name, email) VALUES (?, ?)", (name, email)
)
return {"id": cursor.lastrowid, "name": name, "email": email}
class NotificationWorker:
def __init__(self, repo: UserRepository, email_svc: EmailService):
self.repo = repo
self.email_svc = email_svc
def notify_user(self, email: str, message: str) -> bool:
user = self.repo.find_by_email(email)
if user is None:
return False
self.email_svc.send(
to=user["email"],
subject="Notification",
body=message,
)
return True
class ConfigLoader:
_config: dict[str, Any] | None = None
@classmethod
def load(cls, path: str = "config.json") -> dict[str, Any]:
if cls._config is None:
with open(path, encoding="utf-8") as f:
cls._config = json.load(f)
return cls._config
@property
def debug(self) -> bool:
return os.environ.get("DEBUG", "0") == "1"
async def fetch_user_async(session, user_id: int) -> dict:
async with session.get(f"/users/{user_id}") as resp:
return await resp.json()
# ── 1. Basic mocker.patch ─────────────────────────────────────────────────────
class TestEmailService:
def test_send_success(self, mocker):
"""Patch requests.post to avoid real HTTP."""
mock_post = mocker.patch("requests.post")
mock_post.return_value.json.return_value = {"message_id": "abc123"}
mock_post.return_value.raise_for_status.return_value = None
mock_post.return_value.status_code = 200
svc = EmailService("key-xyz", "[email protected]")
result = svc.send("[email protected]", "Hello", "Body text")
assert result["message_id"] == "abc123"
mock_post.assert_called_once()
# Inspect what was sent
_, kwargs = mock_post.call_args
payload = kwargs["json"]
assert payload["to"] == "[email protected]"
assert payload["subject"] == "Hello"
def test_send_raises_on_http_error(self, mocker):
"""Side effect makes raise_for_status raise HTTPError."""
mock_post = mocker.patch("requests.post")
mock_post.return_value.raise_for_status.side_effect = (
requests.HTTPError("429 Too Many Requests")
)
svc = EmailService("key-xyz", "[email protected]")
with pytest.raises(requests.HTTPError, match="429"):
svc.send("[email protected]", "Subject", "Body")
# ── 2. mocker.patch.object ────────────────────────────────────────────────────
class TestUserRepository:
def test_find_by_email_found(self, mocker):
mock_db = mocker.MagicMock()
mock_db.execute.return_value.fetchone.return_value = (1, "Alice", "[email protected]")
repo = UserRepository(mock_db)
result = repo.find_by_email("[email protected]")
assert result == {"id": 1, "name": "Alice", "email": "[email protected]"}
mock_db.execute.assert_called_once_with(
"SELECT id, name, email FROM users WHERE email = ?",
("[email protected]",),
)
def test_find_by_email_not_found(self, mocker):
mock_db = mocker.MagicMock()
mock_db.execute.return_value.fetchone.return_value = None
repo = UserRepository(mock_db)
result = repo.find_by_email("[email protected]")
assert result is None
def test_create_returns_new_user(self, mocker):
mock_db = mocker.MagicMock()
mock_db.execute.return_value.lastrowid = 99
repo = UserRepository(mock_db)
result = repo.create("Bob", "[email protected]")
assert result == {"id": 99, "name": "Bob", "email": "[email protected]"}
# ── 3. Composing mocks in integration-style tests ─────────────────────────────
class TestNotificationWorker:
def test_notify_sends_email_when_user_found(self, mocker):
mock_repo = mocker.MagicMock(spec=UserRepository)
mock_email = mocker.MagicMock(spec=EmailService)
mock_repo.find_by_email.return_value = {
"id": 1, "name": "Alice", "email": "[email protected]"
}
mock_email.send.return_value = {"message_id": "msg-001"}
worker = NotificationWorker(mock_repo, mock_email)
result = worker.notify_user("[email protected]", "Your order shipped!")
assert result is True
mock_email.send.assert_called_once_with(
to="[email protected]",
subject="Notification",
body="Your order shipped!",
)
def test_notify_returns_false_when_user_missing(self, mocker):
mock_repo = mocker.MagicMock(spec=UserRepository)
mock_email = mocker.MagicMock(spec=EmailService)
mock_repo.find_by_email.return_value = None
worker = NotificationWorker(mock_repo, mock_email)
result = worker.notify_user("[email protected]", "Message")
assert result is False
mock_email.send.assert_not_called()
# ── 4. mocker.spy — wrap real implementation ──────────────────────────────────
class TestSpy:
def test_spy_on_method(self, mocker):
"""spy() calls the real function AND records calls."""
mock_db = MagicMock()
mock_db.execute.return_value.fetchone.return_value = (1, "Alice", "[email protected]")
repo = UserRepository(mock_db)
spy = mocker.spy(repo, "find_by_email")
result = repo.find_by_email("[email protected]")
# Real implementation ran (not replaced)
assert result is not None
# Call was tracked
spy.assert_called_once_with("[email protected]")
assert spy.call_count == 1
# ── 5. File and environment mocking ───────────────────────────────────────────
class TestConfigLoader:
def test_load_reads_json_file(self, mocker):
"""mock_open replaces file I/O with an in-memory string."""
config_data = json.dumps({"debug": False, "db": "sqlite:///test.db"})
mocker.patch("builtins.open", mock_open(read_data=config_data))
# Reset cached class variable
ConfigLoader._config = None
config = ConfigLoader.load("config.json")
assert config["db"] == "sqlite:///test.db"
def test_debug_property_from_env(self, mocker):
"""patch.dict replaces environment variables for the test."""
mocker.patch.dict(os.environ, {"DEBUG": "1"})
loader = ConfigLoader()
assert loader.debug is True
def test_debug_property_off_by_default(self, mocker):
mocker.patch.dict(os.environ, {}, clear=True)
loader = ConfigLoader()
assert loader.debug is False
# ── 6. Async mocking ──────────────────────────────────────────────────────────
class TestAsyncMock:
@pytest.mark.asyncio
async def test_fetch_user_async(self, mocker):
"""mocker.patch auto-detects async coroutines and creates AsyncMock."""
mock_session = mocker.MagicMock()
mock_resp = mocker.AsyncMock()
mock_resp.json.return_value = {"id": 42, "name": "Alice"}
# __aenter__ / __aexit__ for async context manager
mock_session.get.return_value.__aenter__.return_value = mock_resp
mock_session.get.return_value.__aexit__.return_value = None
result = await fetch_user_async(mock_session, 42)
assert result["id"] == 42
assert result["name"] == "Alice"
mock_session.get.assert_called_once_with("/users/42")
# ── 7. Multiple calls with side_effect sequence ───────────────────────────────
class TestSideEffectSequence:
def test_retry_on_failure(self, mocker):
"""side_effect as list returns each value in turn."""
mock_post = mocker.patch("requests.post")
# First two calls raise, third succeeds
mock_post.return_value.raise_for_status.side_effect = [
requests.ConnectionError("timeout"),
requests.ConnectionError("timeout"),
None, # success on third attempt
]
mock_post.return_value.json.return_value = {"ok": True}
mock_post.return_value.status_code = 200
# Verify side_effect cycles through the list
svc = EmailService("key", "[email protected]")
with pytest.raises(requests.ConnectionError):
svc.send("[email protected]", "s", "b")
# On the third attempt with a fresh mock value, raise_for_status returns None
mock_post.return_value.raise_for_status.side_effect = None
result = svc.send("[email protected]", "s", "b")
assert result["ok"] is True
For the unittest.mock directly alternative — unittest.mock.patch used as a decorator requires the mock to be passed as a function argument after self, and multiple patches stack in reverse order making argument position errors common, while mocker.patch in pytest registers all patches through pytest’s fixture teardown so they are always cleaned up even when tests raise exceptions — no addCleanup calls, no tearDown methods, no context manager nesting. For the dependency injection without mocking alternative — passing real implementations through constructors works well for unit tests that need real behavior, but mocker.spy achieves both: it wraps the real method so the actual logic runs AND records every call, making it ideal for verifying that a cache was hit exactly once or that a retry happened exactly twice without replacing the underlying implementation. The Claude Skills 360 bundle includes pytest-mock skill sets covering mocker.patch and patch.object, autospec for signature-preserving mocks, spy for call tracking with real implementation, AsyncMock for coroutines, mock_open for file I/O, patch.dict for environment variables, side_effect sequences, call_args_list inspection, assert_called_once_with patterns, and PropertyMock for property descriptors. Start with the free tier to try test mocking code generation.