Mocking APIs, Files, and Commands
What You'll Learn
How to replace real external dependencies (APIs, files, databases, shell commands) with fakes during testing — so your tests are fast, isolated, and deterministic.
Why Mock?
Without mocking, tests that call real APIs:
- Fail when the internet is down
- Are slow (network round trips)
- Cost money (API usage)
- Leave side effects (test emails, test records)
- Break in CI/CD environments
Mocking replaces the real dependency with a controlled fake.
unittest.mock — Built-In Mocking
from unittest.mock import Mock, MagicMock, patch
Mock — A Flexible Fake Object
from unittest.mock import Mock
# Create a mock
api = Mock()
# Configure what it returns
api.get_user.return_value = {"id": 42, "name": "Alice"}
# Call it
result = api.get_user(42)
print(result) # {"id": 42, "name": "Alice"}
# Verify it was called
api.get_user.assert_called_once_with(42)
api.get_user.assert_called_with(42)
print(api.get_user.call_count) # 1
Mocking Exceptions
api = Mock()
api.get_user.side_effect = ConnectionError("Network unreachable")
# Now calling api.get_user() raises ConnectionError
try:
api.get_user(42)
except ConnectionError as e:
print(f"Got expected error: {e}")
patch — Replace During the Test
patch temporarily replaces a name in a module with a mock:
# myapp/notifier.py
import requests
def send_alert(message: str) -> bool:
response = requests.post("https://alerts.example.com/send",
json={"message": message})
return response.status_code == 200
# test_notifier.py
from unittest.mock import patch, Mock
from myapp.notifier import send_alert
def test_send_alert_success():
with patch("myapp.notifier.requests.post") as mock_post:
mock_post.return_value = Mock(status_code=200)
result = send_alert("System down!")
assert result is True
mock_post.assert_called_once_with(
"https://alerts.example.com/send",
json={"message": "System down!"}
)
def test_send_alert_failure():
with patch("myapp.notifier.requests.post") as mock_post:
mock_post.return_value = Mock(status_code=500)
assert send_alert("test") is False
Using patch as a Decorator
from unittest.mock import patch
import pytest
@patch("myapp.notifier.requests.post")
def test_send_alert(mock_post):
mock_post.return_value = Mock(status_code=200)
assert send_alert("hello") is True
pytest-mock — Cleaner Syntax
pip install pytest-mock
def test_send_alert(mocker):
mock_post = mocker.patch("myapp.notifier.requests.post")
mock_post.return_value = mocker.Mock(status_code=200)
assert send_alert("hello") is True
mock_post.assert_called_once()
Mocking File I/O
# myapp/reader.py
from pathlib import Path
def load_users(path: Path) -> list[dict]:
import json
return json.loads(path.read_text(encoding="utf-8"))
# test_reader.py
from unittest.mock import patch, mock_open
import json
from myapp.reader import load_users
from pathlib import Path
def test_load_users(mocker):
fake_data = json.dumps([{"id": 1, "name": "Alice"}])
mocker.patch("pathlib.Path.read_text", return_value=fake_data)
users = load_users(Path("fake/path.json"))
assert len(users) == 1
assert users[0]["name"] == "Alice"
Mocking Environment Variables
def test_uses_correct_api_key(mocker):
mocker.patch.dict("os.environ", {"API_KEY": "test-key-123"})
result = get_api_key()
assert result == "test-key-123"
Mocking datetime.now()
# myapp/scheduler.py
from datetime import datetime
def is_business_hours() -> bool:
now = datetime.now()
return 9 <= now.hour < 17
# test_scheduler.py
from unittest.mock import patch
from datetime import datetime
from myapp.scheduler import is_business_hours
def test_is_business_hours_at_noon(mocker):
fake_now = datetime(2024, 1, 15, 12, 0, 0) # noon
mocker.patch("myapp.scheduler.datetime") \
.now.return_value = fake_now
assert is_business_hours() is True
def test_is_not_business_hours_at_midnight(mocker):
fake_now = datetime(2024, 1, 15, 0, 0, 0)
mocker.patch("myapp.scheduler.datetime").now.return_value = fake_now
assert is_business_hours() is False
Mocking subprocess / Shell Commands
# myapp/system.py
import subprocess
def get_disk_usage(path: str) -> int:
result = subprocess.run(["df", "-b", path], capture_output=True, text=True)
line = result.stdout.split("\n")[1]
return int(line.split()[3]) # available bytes
# test_system.py
from unittest.mock import Mock
from myapp.system import get_disk_usage
def test_get_disk_usage(mocker):
fake_output = "Filesystem Size Used Avail\n/dev/sda1 100G 40G 60G\n"
mock_run = mocker.patch("myapp.system.subprocess.run")
mock_run.return_value = Mock(stdout=fake_output)
# Test your parsing logic without running a real `df` command
# (adjust assertion to match your parsing)
Common Mistakes
| Mistake | Fix |
|---|---|
| Patching the wrong path | Patch where the name is used, not where it's defined |
| Not asserting calls | Check assert_called_once_with(...) |
| Over-mocking everything | Mock only external I/O, not your own logic |
| Mock leaking between tests | Use with patch(...) or mocker (auto-resets) |
Mocking datetime directly | Patch it in the module that imports it |
Quick Reference
from unittest.mock import Mock, patch
# Mock object
m = Mock()
m.method.return_value = "value"
m.method.side_effect = ValueError("oops")
m.method.assert_called_once_with(arg)
# patch as context manager
with patch("module.name") as mock:
mock.return_value = "fake"
result = fn_under_test()
# patch as decorator
@patch("module.name")
def test_fn(mock_name):
mock_name.return_value = "fake"
...
# pytest-mock (cleaner)
def test_fn(mocker):
mock = mocker.patch("module.name")
mock.return_value = "fake"
# Env vars
mocker.patch.dict("os.environ", {"KEY": "value"})