mock — Mock Objects
Mock replaces external dependencies (databases, HTTP, file I/O) with fake objects during tests. unittest.mock is part of the standard library, while pytest-mock provides a more convenient interface.
MagicMock Basics
from unittest.mock import MagicMock, patch
# MagicMock: auto-generates all attributes and methods
mock_db = MagicMock()
# Specify return values
mock_db.find_user.return_value = {"id": 1, "name": "Alice"}
mock_db.count.return_value = 42
# Use it
user = mock_db.find_user(user_id=1)
assert user["name"] == "Alice"
assert mock_db.count() == 42
# Verify calls
mock_db.find_user.assert_called_once()
mock_db.find_user.assert_called_with(user_id=1)
mock_db.count.assert_called_once()
# Call count
assert mock_db.find_user.call_count == 1
# Simulate exceptions
mock_db.delete.side_effect = PermissionError("No delete permission")
import pytest
with pytest.raises(PermissionError, match="No delete permission"):
mock_db.delete(user_id=1)
patch — Replace a Specific Path
from unittest.mock import patch, MagicMock
import pytest
# Code under test
# user_service.py
def get_user_from_db(user_id: int) -> dict:
import database # real DB module (replaced with mock in tests)
return database.query(f"SELECT * FROM users WHERE id={user_id}")
# @patch: replaces mock only during function execution
@patch("database.query")
def test_get_user(mock_query):
mock_query.return_value = {"id": 1, "name": "Alice"}
from user_service import get_user_from_db
user = get_user_from_db(1)
assert user["name"] == "Alice"
mock_query.assert_called_once_with("SELECT * FROM users WHERE id=1")
# Context manager style
def test_get_user_context():
with patch("database.query") as mock_query:
mock_query.return_value = {"id": 2, "name": "Bob"}
# test code...
# Apply multiple mocks at once (args passed in reverse order)
@patch("module.ClassB")
@patch("module.ClassA")
def test_multiple_mocks(mock_a, mock_b):
# mock_a = ClassA, mock_b = ClassB
mock_a.return_value.method.return_value = "from A"
mock_b.return_value.method.return_value = "from B"
assert mock_a.return_value.method() == "from A"
patch.object — Replace a Method on a Specific Object
from unittest.mock import patch, MagicMock
import requests
class WeatherService:
def get_temperature(self, city: str) -> float:
response = requests.get(f"https://api.weather.com/{city}")
return response.json()["temp"]
def test_get_temperature():
service = WeatherService()
with patch.object(service, "get_temperature", return_value=25.5) as mock_method:
temp = service.get_temperature("Seoul")
assert temp == 25.5
mock_method.assert_called_once_with("Seoul")
# Mock all of requests.get
def test_http_request():
mock_response = MagicMock()
mock_response.json.return_value = {"temp": 22.0}
mock_response.status_code = 200
with patch("requests.get", return_value=mock_response):
service = WeatherService()
temp = service.get_temperature("Busan")
assert temp == 22.0
pytest-mock (mocker fixture)
# pip install pytest-mock
import pytest
class EmailService:
def send(self, to: str, subject: str, body: str) -> bool:
# In production, sends via SMTP server
import smtplib
...
return True
class UserService:
def __init__(self, email_service: EmailService):
self.email_service = email_service
def register(self, email: str, name: str) -> dict:
user = {"id": 1, "email": email, "name": name}
self.email_service.send(
to=email,
subject="Welcome",
body=f"Hello {name}",
)
return user
# mocker fixture (pytest-mock)
def test_register_sends_email(mocker):
mock_email = mocker.MagicMock(spec=EmailService)
mock_email.send.return_value = True
service = UserService(email_service=mock_email)
user = service.register("alice@example.com", "Alice")
assert user["name"] == "Alice"
mock_email.send.assert_called_once_with(
to="alice@example.com",
subject="Welcome",
body="Hello Alice",
)
# mocker.patch: path-based patching
def test_with_mocker_patch(mocker):
mocker.patch("smtplib.SMTP")
email = EmailService()
result = email.send("test@test.com", "Test", "Body")
# smtplib.SMTP replaced with mock
# spy: real function call + call recording
def test_spy(mocker):
service = UserService(email_service=EmailService())
spy = mocker.spy(service, "register")
service.register("test@test.com", "Test")
spy.assert_called_once()
assert spy.call_count == 1
Using side_effect
from unittest.mock import MagicMock
import pytest
# Return different values in sequence
mock = MagicMock()
mock.side_effect = [1, 2, 3, StopIteration]
assert mock() == 1
assert mock() == 2
assert mock() == 3
with pytest.raises(StopIteration):
mock()
# Implement behavior with a function
def smart_side_effect(url: str) -> dict:
if "error" in url:
raise ConnectionError("Connection failed")
return {"url": url, "status": 200}
mock_get = MagicMock(side_effect=smart_side_effect)
result = mock_get("https://example.com/api")
assert result["status"] == 200
with pytest.raises(ConnectionError):
mock_get("https://example.com/error")
# Test retry logic
def fetch_with_retry(url: str, retries: int = 3) -> dict:
import requests
for attempt in range(retries):
try:
return requests.get(url).json()
except ConnectionError:
if attempt == retries - 1:
raise
raise RuntimeError("Unreachable")
def test_retry_logic(mocker):
mock_get = mocker.patch("requests.get")
mock_response = MagicMock()
mock_response.json.return_value = {"data": "ok"}
# Fail twice, succeed on third attempt
mock_get.side_effect = [
ConnectionError(),
ConnectionError(),
mock_response,
]
result = fetch_with_retry("https://example.com")
assert result["data"] == "ok"
assert mock_get.call_count == 3
Mocking Time and Environment
from unittest.mock import patch
import datetime
import os
def get_current_year() -> int:
return datetime.datetime.now().year
def test_mock_datetime(mocker):
mock_now = mocker.patch("datetime.datetime")
mock_now.now.return_value = datetime.datetime(2024, 6, 15, 12, 0, 0)
assert get_current_year() == 2024
# Environment variable mock
def get_config() -> dict:
return {
"api_key": os.environ.get("API_KEY", ""),
"debug": os.environ.get("DEBUG", "false") == "true",
}
def test_env_mock(monkeypatch):
monkeypatch.setenv("API_KEY", "test-key-123")
monkeypatch.setenv("DEBUG", "true")
config = get_config()
assert config["api_key"] == "test-key-123"
assert config["debug"] is True
# File system mock
def read_config_file(path: str) -> str:
with open(path) as f:
return f.read()
def test_file_mock(mocker):
mock_open = mocker.mock_open(read_data="key=value\ndebug=true")
mocker.patch("builtins.open", mock_open)
content = read_config_file("/etc/config.ini")
assert "key=value" in content
Summary
| Tool | Purpose |
|---|---|
MagicMock() | Mock object with auto-generated attributes/methods |
return_value | Specify return value |
side_effect | Raise exception or return dynamically |
@patch("path") | Replace a global object/function |
patch.object(obj, "method") | Replace a method on a specific object |
mocker.patch | pytest-mock style patching |
mocker.spy | Real execution + call recording |
assert_called_once_with(...) | Verify call arguments |
monkeypatch | Temporarily replace env vars and attributes |
Mock objects isolate external dependencies to make tests fast and predictable. However, excessive mocking can drift from real behavior — use them judiciously.