Skip to main content

Mocking with unittest.mock

Introduction​

Mocking is a testing technique that replaces parts of your system with mock objects to isolate the code you want to test. When testing complex systems with external dependencies like databases, APIs, or file systems, mocking allows you to test your logic without relying on these external systems.

Python's unittest.mock module provides powerful mocking capabilities that make it easy to:

  • Isolate units - Test individual components independently
  • Control behavior - Define exact responses from dependencies
  • Verify interactions - Check how your code calls external systems
  • Test edge cases - Simulate error conditions and unusual scenarios
  • Speed up tests - Remove slow external calls

Mocking is essential for writing fast, reliable, and maintainable unit tests that focus on your code's logic rather than external system behavior.


Concepts​

Mock Objects

  • Mock() - Generic mock object that records how it's used
  • MagicMock() - Mock with magic methods pre-configured
  • patch() - Temporarily replace objects during testing
  • return_value - Set what a mock returns when called

Patching Strategies

  • @patch decorator - Apply to test functions or classes
  • patch() context manager - Use with with statements
  • patch.object() - Patch specific object attributes
  • patch.multiple() - Patch multiple targets at once

Assertion Methods

  • assert_called() - Verify mock was called
  • assert_called_once() - Verify called exactly once
  • assert_called_with() - Check call arguments
  • assert_not_called() - Verify mock wasn't called
  • call_count - Number of times mock was called

Advanced Features

  • side_effect - Raise exceptions or return different values
  • spec - Limit mock to existing object interface
  • autospec - Automatically create spec from real object
  • return_value vs side_effect - Different ways to control behavior

Examples​

Basic Mock Usage

from unittest.mock import Mock

# Create a simple mock
mock_function = Mock()
mock_function.return_value = "Hello, World!"

# Use the mock
result = mock_function()
print(result) # "Hello, World!"

# Verify it was called
assert mock_function.called
assert mock_function.call_count == 1
mock_function.assert_called_once()

Mocking External Dependencies

# weather_service.py
import requests

class WeatherService:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://api.weather.com"

def get_temperature(self, city):
"""Get current temperature for a city"""
url = f"{self.base_url}/current"
params = {"city": city, "key": self.api_key}

try:
response = requests.get(url, params=params)
response.raise_for_status()
data = response.json()
return data["temperature"]
except requests.RequestException as e:
raise ConnectionError(f"Failed to fetch weather data: {e}")

def is_hot(self, city, threshold=25):
"""Check if it's hot in a city"""
temperature = self.get_temperature(city)
return temperature > threshold

# test_weather_service.py
import pytest
from unittest.mock import Mock, patch
from weather_service import WeatherService

class TestWeatherService:

def setup_method(self):
self.weather_service = WeatherService("test_api_key")

@patch('weather_service.requests.get')
def test_get_temperature_success(self, mock_get):
# Setup mock response
mock_response = Mock()
mock_response.json.return_value = {"temperature": 22.5}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response

# Test the method
temperature = self.weather_service.get_temperature("London")

# Assertions
assert temperature == 22.5
mock_get.assert_called_once_with(
"https://api.weather.com/current",
params={"city": "London", "key": "test_api_key"}
)

@patch('weather_service.requests.get')
def test_get_temperature_api_error(self, mock_get):
# Setup mock to raise exception
mock_get.side_effect = requests.RequestException("API Error")

# Test exception handling
with pytest.raises(ConnectionError, match="Failed to fetch weather data"):
self.weather_service.get_temperature("London")

def test_is_hot(self):
# Mock the get_temperature method directly
with patch.object(self.weather_service, 'get_temperature') as mock_get_temp:
mock_get_temp.return_value = 30.0

# Test the method
result = self.weather_service.is_hot("Madrid")

# Assertions
assert result == True
mock_get_temp.assert_called_once_with("Madrid")

Database Mocking Example

# user_repository.py
import sqlite3
from typing import Optional, List

class User:
def __init__(self, id: int, name: str, email: str):
self.id = id
self.name = name
self.email = email

def to_dict(self):
return {"id": self.id, "name": self.name, "email": self.email}

class UserRepository:
def __init__(self, db_path: str):
self.db_path = db_path

def get_connection(self):
return sqlite3.connect(self.db_path)

def create_user(self, name: str, email: str) -> User:
"""Create a new user"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
(name, email)
)
user_id = cursor.lastrowid
conn.commit()
return User(user_id, name, email)

def get_user(self, user_id: int) -> Optional[User]:
"""Get user by ID"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT id, name, email FROM users WHERE id = ?", (user_id,))
row = cursor.fetchone()

if row:
return User(*row)
return None

def get_users_by_domain(self, domain: str) -> List[User]:
"""Get all users with email from specific domain"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT id, name, email FROM users WHERE email LIKE ?",
(f"%@{domain}",)
)
rows = cursor.fetchall()
return [User(*row) for row in rows]

# test_user_repository.py
import pytest
from unittest.mock import Mock, patch, MagicMock
from user_repository import UserRepository, User

class TestUserRepository:

def setup_method(self):
self.repo = UserRepository("test.db")

@patch('user_repository.sqlite3.connect')
def test_create_user(self, mock_connect):
# Setup mock database connection
mock_conn = MagicMock()
mock_cursor = MagicMock()
mock_cursor.lastrowid = 123
mock_conn.cursor.return_value = mock_cursor
mock_connect.return_value.__enter__.return_value = mock_conn

# Test user creation
user = self.repo.create_user("John Doe", "john@example.com")

# Assertions
assert user.id == 123
assert user.name == "John Doe"
assert user.email == "john@example.com"

# Verify database interactions
mock_cursor.execute.assert_called_once_with(
"INSERT INTO users (name, email) VALUES (?, ?)",
("John Doe", "john@example.com")
)
mock_conn.commit.assert_called_once()

@patch('user_repository.sqlite3.connect')
def test_get_user_found(self, mock_connect):
# Setup mock to return user data
mock_conn = MagicMock()
mock_cursor = MagicMock()
mock_cursor.fetchone.return_value = (1, "Alice Smith", "alice@test.com")
mock_conn.cursor.return_value = mock_cursor
mock_connect.return_value.__enter__.return_value = mock_conn

# Test getting user
user = self.repo.get_user(1)

# Assertions
assert user is not None
assert user.id == 1
assert user.name == "Alice Smith"
assert user.email == "alice@test.com"

mock_cursor.execute.assert_called_once_with(
"SELECT id, name, email FROM users WHERE id = ?", (1,)
)

@patch('user_repository.sqlite3.connect')
def test_get_user_not_found(self, mock_connect):
# Setup mock to return None
mock_conn = MagicMock()
mock_cursor = MagicMock()
mock_cursor.fetchone.return_value = None
mock_conn.cursor.return_value = mock_cursor
mock_connect.return_value.__enter__.return_value = mock_conn

# Test getting non-existent user
user = self.repo.get_user(999)

# Assertions
assert user is None
mock_cursor.execute.assert_called_once_with(
"SELECT id, name, email FROM users WHERE id = ?", (999,)
)

Mock Side Effects and Complex Scenarios

# email_service.py
import smtplib
from email.mime.text import MIMEText
from typing import List

class EmailService:
def __init__(self, smtp_host: str, smtp_port: int):
self.smtp_host = smtp_host
self.smtp_port = smtp_port

def send_email(self, to_email: str, subject: str, body: str):
"""Send a single email"""
try:
with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
msg = MIMEText(body)
msg['Subject'] = subject
msg['To'] = to_email
server.send_message(msg)
return True
except Exception as e:
return False

def send_bulk_emails(self, recipients: List[str], subject: str, body: str):
"""Send emails to multiple recipients"""
results = []
for recipient in recipients:
success = self.send_email(recipient, subject, body)
results.append((recipient, success))
return results

# test_email_service.py
import pytest
from unittest.mock import patch, Mock, call
from email_service import EmailService

class TestEmailService:

def setup_method(self):
self.email_service = EmailService("smtp.gmail.com", 587)

@patch('email_service.smtplib.SMTP')
def test_send_email_success(self, mock_smtp):
# Setup mock SMTP server
mock_server = Mock()
mock_smtp.return_value.__enter__.return_value = mock_server

# Test successful email sending
result = self.email_service.send_email(
"test@example.com",
"Test Subject",
"Test Body"
)

# Assertions
assert result == True
mock_smtp.assert_called_once_with("smtp.gmail.com", 587)
mock_server.send_message.assert_called_once()

@patch('email_service.smtplib.SMTP')
def test_send_email_failure(self, mock_smtp):
# Setup mock to raise exception
mock_smtp.side_effect = Exception("SMTP Error")

# Test email failure
result = self.email_service.send_email(
"test@example.com",
"Test Subject",
"Test Body"
)

# Assertions
assert result == False

def test_send_bulk_emails(self):
# Mock the send_email method with different results
with patch.object(self.email_service, 'send_email') as mock_send:
# Configure side_effect for multiple calls
mock_send.side_effect = [True, False, True]

recipients = ["user1@test.com", "user2@test.com", "user3@test.com"]
results = self.email_service.send_bulk_emails(
recipients, "Bulk Subject", "Bulk Body"
)

# Assertions
expected_results = [
("user1@test.com", True),
("user2@test.com", False),
("user3@test.com", True)
]
assert results == expected_results

# Verify all calls were made
expected_calls = [
call("user1@test.com", "Bulk Subject", "Bulk Body"),
call("user2@test.com", "Bulk Subject", "Bulk Body"),
call("user3@test.com", "Bulk Subject", "Bulk Body")
]
mock_send.assert_has_calls(expected_calls)

Property and Attribute Mocking

# file_processor.py
import os
import json
from datetime import datetime

class FileProcessor:
def __init__(self, directory: str):
self.directory = directory

def process_file(self, filename: str):
"""Process a file and return metadata"""
file_path = os.path.join(self.directory, filename)

if not os.path.exists(file_path):
raise FileNotFoundError(f"File not found: {file_path}")

file_size = os.path.getsize(file_path)
modified_time = os.path.getmtime(file_path)

with open(file_path, 'r') as f:
content = f.read()

return {
"filename": filename,
"size": file_size,
"modified": datetime.fromtimestamp(modified_time).isoformat(),
"content": content,
"processed_at": datetime.now().isoformat()
}

# test_file_processor.py
import pytest
from unittest.mock import patch, mock_open, Mock
from datetime import datetime
from file_processor import FileProcessor

class TestFileProcessor:

def setup_method(self):
self.processor = FileProcessor("/test/directory")

@patch('file_processor.datetime')
@patch('file_processor.os.path.getmtime')
@patch('file_processor.os.path.getsize')
@patch('file_processor.os.path.exists')
@patch('builtins.open', new_callable=mock_open, read_data="test content")
def test_process_file_success(self, mock_file, mock_exists, mock_getsize,
mock_getmtime, mock_datetime):
# Setup mocks
mock_exists.return_value = True
mock_getsize.return_value = 12
mock_getmtime.return_value = 1609459200 # 2021-01-01 timestamp

# Mock datetime
mock_datetime.fromtimestamp.return_value.isoformat.return_value = "2021-01-01T00:00:00"
mock_datetime.now.return_value.isoformat.return_value = "2024-01-01T12:00:00"

# Test processing
result = self.processor.process_file("test.txt")

# Assertions
expected = {
"filename": "test.txt",
"size": 12,
"modified": "2021-01-01T00:00:00",
"content": "test content",
"processed_at": "2024-01-01T12:00:00"
}
assert result == expected

# Verify method calls
mock_exists.assert_called_once_with("/test/directory/test.txt")
mock_getsize.assert_called_once_with("/test/directory/test.txt")
mock_file.assert_called_once_with("/test/directory/test.txt", 'r')

@patch('file_processor.os.path.exists')
def test_process_file_not_found(self, mock_exists):
# Setup mock for non-existent file
mock_exists.return_value = False

# Test file not found scenario
with pytest.raises(FileNotFoundError, match="File not found"):
self.processor.process_file("nonexistent.txt")

Challenges​

1. Mock HTTP Client​

Create a NewsService class that fetches news from an API and write tests that mock the HTTP requests.

💡 Show Answer
# news_service.py
import requests
from typing import List, Dict

class NewsService:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://newsapi.org/v2"

def get_headlines(self, country: str = "us") -> List[Dict]:
"""Get top headlines for a country"""
url = f"{self.base_url}/top-headlines"
params = {
"country": country,
"apiKey": self.api_key
}

response = requests.get(url, params=params)
response.raise_for_status()

data = response.json()
return data.get("articles", [])

def search_news(self, query: str, page_size: int = 20) -> Dict:
"""Search for news articles"""
url = f"{self.base_url}/everything"
params = {
"q": query,
"pageSize": page_size,
"apiKey": self.api_key
}

response = requests.get(url, params=params)
response.raise_for_status()

return response.json()

# test_news_service.py
import pytest
from unittest.mock import patch, Mock
from news_service import NewsService

class TestNewsService:

def setup_method(self):
self.news_service = NewsService("test_api_key")

@patch('news_service.requests.get')
def test_get_headlines_success(self, mock_get):
# Setup mock response
mock_response = Mock()
mock_response.json.return_value = {
"articles": [
{"title": "Test News 1", "url": "http://test1.com"},
{"title": "Test News 2", "url": "http://test2.com"}
]
}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response

# Test the method
articles = self.news_service.get_headlines("uk")

# Assertions
assert len(articles) == 2
assert articles[0]["title"] == "Test News 1"
mock_get.assert_called_once_with(
"https://newsapi.org/v2/top-headlines",
params={"country": "uk", "apiKey": "test_api_key"}
)

@patch('news_service.requests.get')
def test_search_news(self, mock_get):
# Setup mock response
mock_response = Mock()
mock_response.json.return_value = {
"totalResults": 1,
"articles": [{"title": "Python News"}]
}
mock_get.return_value = mock_response

# Test search
result = self.news_service.search_news("python", page_size=10)

# Assertions
assert result["totalResults"] == 1
mock_get.assert_called_once_with(
"https://newsapi.org/v2/everything",
params={"q": "python", "pageSize": 10, "apiKey": "test_api_key"}
)

2. Mock File Operations​

Create a ConfigManager class that reads/writes configuration files and mock the file operations.

💡 Show Answer
# config_manager.py
import json
import os
from typing import Dict, Any

class ConfigManager:
def __init__(self, config_path: str):
self.config_path = config_path
self._config = {}

def load_config(self) -> Dict[str, Any]:
"""Load configuration from file"""
if not os.path.exists(self.config_path):
return {}

with open(self.config_path, 'r') as f:
self._config = json.load(f)
return self._config.copy()

def save_config(self, config: Dict[str, Any]) -> bool:
"""Save configuration to file"""
try:
# Create directory if it doesn't exist
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)

with open(self.config_path, 'w') as f:
json.dump(config, f, indent=2)

self._config = config.copy()
return True
except Exception:
return False

def get_setting(self, key: str, default=None):
"""Get a specific setting"""
return self._config.get(key, default)

def update_setting(self, key: str, value: Any) -> bool:
"""Update a single setting"""
self._config[key] = value
return self.save_config(self._config)

# test_config_manager.py
import pytest
from unittest.mock import patch, mock_open, Mock
import json
from config_manager import ConfigManager

class TestConfigManager:

def setup_method(self):
self.config_manager = ConfigManager("/test/config.json")

@patch('config_manager.os.path.exists')
@patch('builtins.open', new_callable=mock_open)
def test_load_config_success(self, mock_file, mock_exists):
# Setup mocks
mock_exists.return_value = True
test_config = {"debug": True, "host": "localhost"}
mock_file.return_value.read.return_value = json.dumps(test_config)

# Mock json.load
with patch('config_manager.json.load', return_value=test_config):
result = self.config_manager.load_config()

# Assertions
assert result == test_config
mock_exists.assert_called_once_with("/test/config.json")
mock_file.assert_called_once_with("/test/config.json", 'r')

@patch('config_manager.os.path.exists')
def test_load_config_file_not_exists(self, mock_exists):
# Setup mock for non-existent file
mock_exists.return_value = False

# Test loading non-existent config
result = self.config_manager.load_config()

# Assertions
assert result == {}

@patch('config_manager.os.makedirs')
@patch('config_manager.os.path.dirname')
@patch('builtins.open', new_callable=mock_open)
def test_save_config_success(self, mock_file, mock_dirname, mock_makedirs):
# Setup mocks
mock_dirname.return_value = "/test"
test_config = {"debug": False, "port": 8080}

# Mock json.dump
with patch('config_manager.json.dump') as mock_dump:
result = self.config_manager.save_config(test_config)

# Assertions
assert result == True
mock_makedirs.assert_called_once_with("/test", exist_ok=True)
mock_file.assert_called_once_with("/test/config.json", 'w')
mock_dump.assert_called_once_with(test_config, mock_file.return_value, indent=2)

def test_get_setting(self):
# Setup internal config
self.config_manager._config = {"timeout": 30, "retries": 3}

# Test getting existing setting
assert self.config_manager.get_setting("timeout") == 30

# Test getting non-existent setting with default
assert self.config_manager.get_setting("missing", "default") == "default"

def test_update_setting(self):
# Mock save_config method
with patch.object(self.config_manager, 'save_config', return_value=True) as mock_save:
result = self.config_manager.update_setting("new_key", "new_value")

# Assertions
assert result == True
assert self.config_manager._config["new_key"] == "new_value"
mock_save.assert_called_once_with({"new_key": "new_value"})

3. Mock Class Dependencies​

Create a OrderProcessor class that depends on PaymentService and InventoryService, then mock these dependencies.

💡 Show Answer
# order_processor.py
from typing import Dict, List
from enum import Enum

class PaymentStatus(Enum):
SUCCESS = "success"
FAILED = "failed"
PENDING = "pending"

class PaymentService:
def process_payment(self, amount: float, card_token: str) -> Dict:
"""Process payment - would normally call external payment API"""
# This would be implemented to call real payment service
pass

class InventoryService:
def check_stock(self, product_id: str, quantity: int) -> bool:
"""Check if product is in stock"""
# This would be implemented to check real inventory
pass

def reserve_items(self, product_id: str, quantity: int) -> bool:
"""Reserve items for order"""
# This would be implemented to update real inventory
pass

def release_reservation(self, product_id: str, quantity: int) -> bool:
"""Release reserved items"""
# This would be implemented to update real inventory
pass

class OrderProcessor:
def __init__(self, payment_service: PaymentService, inventory_service: InventoryService):
self.payment_service = payment_service
self.inventory_service = inventory_service

def process_order(self, order_data: Dict) -> Dict:
"""Process a complete order"""
product_id = order_data["product_id"]
quantity = order_data["quantity"]
amount = order_data["amount"]
card_token = order_data["card_token"]

# Check stock
if not self.inventory_service.check_stock(product_id, quantity):
return {"status": "failed", "reason": "Insufficient stock"}

# Reserve items
if not self.inventory_service.reserve_items(product_id, quantity):
return {"status": "failed", "reason": "Could not reserve items"}

# Process payment
try:
payment_result = self.payment_service.process_payment(amount, card_token)

if payment_result["status"] == PaymentStatus.SUCCESS.value:
return {
"status": "success",
"order_id": f"ORDER_{product_id}_{quantity}",
"payment_id": payment_result.get("payment_id")
}
else:
# Release reservation on payment failure
self.inventory_service.release_reservation(product_id, quantity)
return {"status": "failed", "reason": "Payment failed"}

except Exception as e:
# Release reservation on any error
self.inventory_service.release_reservation(product_id, quantity)
return {"status": "failed", "reason": f"Processing error: {str(e)}"}

# test_order_processor.py
import pytest
from unittest.mock import Mock, MagicMock
from order_processor import OrderProcessor, PaymentService, InventoryService, PaymentStatus

class TestOrderProcessor:

def setup_method(self):
self.payment_service = Mock(spec=PaymentService)
self.inventory_service = Mock(spec=InventoryService)
self.processor = OrderProcessor(self.payment_service, self.inventory_service)

self.sample_order = {
"product_id": "PROD123",
"quantity": 2,
"amount": 99.99,
"card_token": "card_token_123"
}

def test_process_order_success(self):
# Setup mocks for successful scenario
self.inventory_service.check_stock.return_value = True
self.inventory_service.reserve_items.return_value = True
self.payment_service.process_payment.return_value = {
"status": "success",
"payment_id": "PAY123"
}

# Process order
result = self.processor.process_order(self.sample_order)

# Assertions
assert result["status"] == "success"
assert result["order_id"] == "ORDER_PROD123_2"
assert result["payment_id"] == "PAY123"

# Verify method calls
self.inventory_service.check_stock.assert_called_once_with("PROD123", 2)
self.inventory_service.reserve_items.assert_called_once_with("PROD123", 2)
self.payment_service.process_payment.assert_called_once_with(99.99, "card_token_123")

def test_process_order_insufficient_stock(self):
# Setup mock for insufficient stock
self.inventory_service.check_stock.return_value = False

# Process order
result = self.processor.process_order(self.sample_order)

# Assertions
assert result["status"] == "failed"
assert result["reason"] == "Insufficient stock"

# Verify only stock check was called
self.inventory_service.check_stock.assert_called_once_with("PROD123", 2)
self.inventory_service.reserve_items.assert_not_called()
self.payment_service.process_payment.assert_not_called()

def test_process_order_reservation_failed(self):
# Setup mocks
self.inventory_service.check_stock.return_value = True
self.inventory_service.reserve_items.return_value = False

# Process order
result = self.processor.process_order(self.sample_order)

# Assertions
assert result["status"] == "failed"
assert result["reason"] == "Could not reserve items"

# Verify payment was not attempted
self.payment_service.process_payment.assert_not_called()

def test_process_order_payment_failed(self):
# Setup mocks
self.inventory_service.check_stock.return_value = True
self.inventory_service.reserve_items.return_value = True
self.inventory_service.release_reservation.return_value = True
self.payment_service.process_payment.return_value = {
"status": "failed",
"error": "Card declined"
}

# Process order
result = self.processor.process_order(self.sample_order)

# Assertions
assert result["status"] == "failed"
assert result["reason"] == "Payment failed"

# Verify reservation was released
self.inventory_service.release_reservation.assert_called_once_with("PROD123", 2)

def test_process_order_payment_exception(self):
# Setup mocks
self.inventory_service.check_stock.return_value = True
self.inventory_service.reserve_items.return_value = True
self.inventory_service.release_reservation.return_value = True
self.payment_service.process_payment.side_effect = Exception("Network error")

# Process order
result = self.processor.process_order(self.sample_order)

# Assertions
assert result["status"] == "failed"
assert "Processing error: Network error" in result["reason"]

# Verify reservation was released
self.inventory_service.release_reservation.assert_called_once_with("PROD123", 2)

4. Advanced Mocking with autospec​

Create tests using autospec to ensure mocks match the real interface and catch interface changes.

💡 Show Answer
# calculator_service.py
import math
from typing import List, Union

class Calculator:
"""Advanced calculator with scientific functions"""

def __init__(self, precision: int = 2):
self.precision = precision
self.history = []

def add(self, a: float, b: float) -> float:
"""Add two numbers"""
result = round(a + b, self.precision)
self.history.append(f"add({a}, {b}) = {result}")
return result

def divide(self, a: float, b: float) -> float:
"""Divide two numbers"""
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
result = round(a / b, self.precision)
self.history.append(f"divide({a}, {b}) = {result}")
return result

def sqrt(self, number: float) -> float:
"""Calculate square root"""
if number < 0:
raise ValueError("Cannot calculate square root of negative number")
result = round(math.sqrt(number), self.precision)
self.history.append(f"sqrt({number}) = {result}")
return result

def get_history(self) -> List[str]:
"""Get calculation history"""
return self.history.copy()

class CalculatorService:
"""Service that uses Calculator for complex operations"""

def __init__(self, calculator: Calculator):
self.calculator = calculator

def calculate_average(self, numbers: List[float]) -> float:
"""Calculate average of numbers"""
if not numbers:
raise ValueError("Cannot calculate average of empty list")

total = numbers[0]
for num in numbers[1:]:
total = self.calculator.add(total, num)

return self.calculator.divide(total, len(numbers))

def calculate_standard_deviation(self, numbers: List[float]) -> float:
"""Calculate standard deviation"""
if len(numbers) < 2:
raise ValueError("Need at least 2 numbers for standard deviation")

# Calculate mean
mean = self.calculate_average(numbers)

# Calculate sum of squared differences
sum_squared_diff = 0
for num in numbers:
diff = self.calculator.add(num, -mean) # num - mean
squared_diff = diff * diff # We'll use direct multiplication
sum_squared_diff = self.calculator.add(sum_squared_diff, squared_diff)

# Calculate variance
variance = self.calculator.divide(sum_squared_diff, len(numbers) - 1)

# Return standard deviation
return self.calculator.sqrt(variance)

# test_calculator_service.py
import pytest
from unittest.mock import create_autospec, Mock
from calculator_service import CalculatorService, Calculator

class TestCalculatorService:

def setup_method(self):
# Create autospec mock of Calculator
self.mock_calculator = create_autospec(Calculator, instance=True)
self.service = CalculatorService(self.mock_calculator)

def test_calculate_average_success(self):
# Setup mock behaviors
self.mock_calculator.add.side_effect = lambda a, b: a + b
self.mock_calculator.divide.return_value = 15.0

# Test calculation
result = self.service.calculate_average([10, 20, 15])

# Assertions
assert result == 15.0

# Verify add was called correctly
expected_add_calls = [
((10, 20), {}),
((30, 15), {})
]
actual_add_calls = [call for call in self.mock_calculator.add.call_args_list]
assert len(actual_add_calls) == 2

# Verify divide was called
self.mock_calculator.divide.assert_called_once_with(45, 3)

def test_calculate_average_empty_list(self):
# Test with empty list
with pytest.raises(ValueError, match="Cannot calculate average of empty list"):
self.service.calculate_average([])

# Verify no calculator methods were called
self.mock_calculator.add.assert_not_called()
self.mock_calculator.divide.assert_not_called()

def test_calculate_standard_deviation(self):
# Setup mock behaviors
# Mock add for calculating total, then differences
add_returns = [30.0, 0.0, 10.0, 100.0, 1000.0] # Sequential returns
self.mock_calculator.add.side_effect = add_returns

# Mock divide for average and variance
divide_returns = [15.0, 500.0] # average, then variance
self.mock_calculator.divide.side_effect = divide_returns

# Mock sqrt for final standard deviation
self.mock_calculator.sqrt.return_value = 22.36

# Test calculation
result = self.service.calculate_standard_deviation([10, 20])

# Assertions
assert result == 22.36

# Verify all methods were called
assert self.mock_calculator.add.call_count > 0
assert self.mock_calculator.divide.call_count == 2
self.mock_calculator.sqrt.assert_called_once_with(500.0)

def test_autospec_prevents_typos(self):
# This test demonstrates that autospec catches attribute errors
with pytest.raises(AttributeError):
# This would raise AttributeError because 'multiply' doesn't exist
# in the real Calculator class
self.mock_calculator.multiply(5, 3)

def test_autospec_checks_method_signatures(self):
# This test shows autospec validates method signatures
with pytest.raises(TypeError):
# This should raise TypeError because add() takes exactly 2 arguments
self.mock_calculator.add(1, 2, 3) # Too many arguments

# Alternative approach using patch with autospec
class TestCalculatorServiceWithPatch:

@patch('calculator_service.Calculator', autospec=True)
def test_with_patch_autospec(self, MockCalculator):
# Create instance mock
mock_calc_instance = MockCalculator.return_value
mock_calc_instance.add.side_effect = lambda a, b: a + b
mock_calc_instance.divide.return_value = 7.5

# Create service
service = CalculatorService(mock_calc_instance)

# Test
result = service.calculate_average([5, 10])

# Verify
assert result == 7.5
MockCalculator.assert_called_once() # Constructor was called
mock_calc_instance.divide.assert_called_once_with(15, 2)

Best Practices​

When to Use Mocking

  • External dependencies - APIs, databases, file systems
  • Slow operations - Network calls, large file processing
  • Non-deterministic behavior - Random numbers, timestamps
  • Error conditions - Test exception handling paths

Mock Guidelines

  • Mock at boundaries - Don't mock your own code extensively
  • Use autospec - Ensures mocks match real interfaces
  • Verify interactions - Check calls, arguments, and counts
  • Keep mocks simple - Focus on behavior, not implementation

Common Pitfalls to Avoid

  • Over-mocking - Testing mocks instead of real logic
  • Brittle tests - Mocks too coupled to implementation
  • Mock everything - Balance between isolation and realism
  • Ignoring return values - Always configure mock behaviors

Remember: mocking is a powerful tool for isolation, but use it judiciously to maintain test value and maintainability.