Skip to main content

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

MistakeFix
Patching the wrong pathPatch where the name is used, not where it's defined
Not asserting callsCheck assert_called_once_with(...)
Over-mocking everythingMock only external I/O, not your own logic
Mock leaking between testsUse with patch(...) or mocker (auto-resets)
Mocking datetime directlyPatch 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"})

What's Next

Lesson 3: Debugging, Profiling, and Linting