| name | test-mocking-rules |
| description | Comprehensive guidelines for when and how to use mocks, stubs, and fakes in tests |
| languages | ["python","typescript","javascript","go","rust","java","csharp","php","ruby"] |
| subagents | ["test/unit","test/integration"] |
| tools_needed | [] |
Test Mocking Rules
Mocking is a testing technique that replaces real implementations with controlled substitutes. This guide defines when to mock, what to mock, and common anti-patterns to avoid.
Core Principle: Mock at Boundaries, Not Internals
The Golden Rule: Mock only external dependencies (I/O boundaries), never internal logic.
Why?
- Mocking internal logic = testing implementation, not behavior
- Over-mocking = brittle tests that break when refactoring
- Under-mocking = slow, flaky tests dependent on external services
When to Mock
✓ Mock These (Process Boundaries)
1. Database Connections
Reason: Tests should be fast, isolated, and not require running database
Example (Python with pytest):
from unittest.mock import Mock
import pytest
def test_get_user_by_id():
mock_db = Mock()
mock_db.query.return_value.filter.return_value.first.return_value = {
'id': '123',
'email': 'test@example.com'
}
user_service = UserService(db=mock_db)
user = user_service.get_user_by_id('123')
assert user['email'] == 'test@example.com'
mock_db.query.assert_called_once()
Exception: Integration tests should use REAL database (test database or in-memory)
2. Network Calls (HTTP, External APIs)
Reason: External APIs are slow, cost money, have rate limits, can be down
Example (TypeScript with Vitest):
import { describe, it, expect, vi } from 'vitest'
import { PaymentService } from './payment-service'
describe('PaymentService', () => {
it('processes payment via Stripe API', async () => {
const mockHttpClient = {
post: vi.fn().mockResolvedValue({
data: { id: 'charge_123', status: 'succeeded' }
})
}
const paymentService = new PaymentService(mockHttpClient)
const result = await paymentService.charge(100, 'tok_visa')
expect(result.status).toBe('succeeded')
expect(mockHttpClient.post).toHaveBeenCalledWith(
'/v1/charges',
expect.objectContaining({ amount: 100, source: 'tok_visa' })
)
})
})
Better Alternative: Use a mock server library (MSW, nock, WireMock)
3. Filesystem Operations
Reason: File I/O is slow, tests should not depend on disk state
Example (Python):
from unittest.mock import mock_open, patch
def test_read_config():
mock_file_content = "database_url=postgres://localhost"
with patch('builtins.open', mock_open(read_data=mock_file_content)):
config = ConfigLoader.load('/etc/config.txt')
assert config['database_url'] == 'postgres://localhost'
Alternative: Use temp directories for integration tests
4. Time/Date Functions
Reason: Tests should be deterministic, not dependent on current time
Example (Python):
from unittest.mock import patch
from datetime import datetime
def test_is_expired():
fixed_time = datetime(2026, 1, 1, 12, 0, 0)
with patch('myapp.utils.datetime') as mock_datetime:
mock_datetime.now.return_value = fixed_time
token = Token(expires_at=datetime(2025, 12, 31))
assert token.is_expired() is True
Better Alternative: Inject clock/time provider
5. Random Number Generation
Reason: Tests should be reproducible
Example (TypeScript):
import { vi } from 'vitest'
it('generates random session ID', () => {
vi.spyOn(Math, 'random').mockReturnValue(0.5)
const sessionId = generateSessionId()
expect(sessionId).toBe('expected-deterministic-id')
})
Better Alternative: Inject random number generator
6. External Services (Email, SMS, Cloud Storage)
Reason: Should not send real emails/SMS during tests
Example (Python):
from unittest.mock import Mock
def test_send_welcome_email():
mock_email_client = Mock()
user_service = UserService(email_client=mock_email_client)
user_service.register_user('test@example.com', 'password')
mock_email_client.send.assert_called_once_with(
to='test@example.com',
subject='Welcome!',
body=expect.any(str)
)
What NOT to Mock
✗ Never Mock These
1. The Thing Under Test
Problem: Defeats the purpose of the test
❌ Wrong:
def test_calculate_total():
mock_calculator = Mock()
mock_calculator.calculate_total.return_value = 100
assert mock_calculator.calculate_total([10, 20, 30]) == 100
✓ Correct:
def test_calculate_total():
calculator = Calculator()
assert calculator.calculate_total([10, 20, 30]) == 60
2. Internal Helper Functions
Problem: Tests implementation, not behavior
❌ Wrong:
def test_process_order():
mock_validate = Mock(return_value=True)
with patch('myapp.order.validate_order', mock_validate):
result = process_order(order_data)
assert result.status == 'processed'
✓ Correct:
def test_process_order():
result = process_order(valid_order_data)
assert result.status == 'processed'
def test_process_order_with_invalid_data():
with pytest.raises(ValidationError):
process_order(invalid_order_data)
Lesson: Test internal helpers indirectly through public API
3. Your Own Database (in integration tests)
Problem: Integration tests should test real database interactions
❌ Wrong:
it('creates user in database', async () => {
const mockDb = { insert: vi.fn().mockResolvedValue({ id: '123' }) }
const userRepo = new UserRepository(mockDb)
await userRepo.create({ email: 'test@example.com' })
})
✓ Correct:
it('creates user in database', async () => {
const testDb = await setupTestDatabase()
const userRepo = new UserRepository(testDb)
const user = await userRepo.create({ email: 'test@example.com' })
const found = await testDb.query('SELECT * FROM users WHERE id = $1', [user.id])
expect(found.email).toBe('test@example.com')
await teardownTestDatabase(testDb)
})
For unit tests: Mock database. For integration tests: Use real database.
4. Language Standard Library
Problem: Standard library is well-tested, mocking adds no value
❌ Wrong:
from unittest.mock import patch
def test_parse_json():
with patch('json.loads', return_value={'key': 'value'}):
result = parse_config('{"key": "value"}')
✓ Correct:
def test_parse_json():
result = parse_config('{"key": "value"}')
assert result['key'] == 'value'
5. Simple Data Structures
Problem: Mocking adds complexity without benefit
❌ Wrong:
it('calculates cart total', () => {
const mockCart = {
items: vi.fn().mockReturnValue([
{ price: 10 },
{ price: 20 }
])
}
})
✓ Correct:
it('calculates cart total', () => {
const cart = new Cart()
cart.addItem({ price: 10 })
cart.addItem({ price: 20 })
expect(cart.total()).toBe(30)
})
Mock Types: Mocks, Stubs, Fakes, Spies
Stub
Definition: Returns fixed data, no behavior verification
When to use: Simple test cases where you only care about return value
Example:
def test_get_user():
stub_db = Mock()
stub_db.get_user.return_value = User(id='123', email='test@example.com')
service = UserService(db=stub_db)
user = service.get_user('123')
assert user.email == 'test@example.com'
Mock
Definition: Returns data AND verifies behavior (method calls, arguments)
When to use: When you need to verify interactions
Example:
def test_send_notification():
mock_email = Mock()
notifier = Notifier(email_client=mock_email)
notifier.send_welcome('test@example.com')
mock_email.send.assert_called_once_with(
to='test@example.com',
subject='Welcome',
body=expect.stringContaining('welcome')
)
Fake
Definition: Working implementation with shortcuts (in-memory database, fake API)
When to use: When you need realistic behavior without external dependencies
Example:
class FakeDatabase {
private users: Map<string, User> = new Map()
async insert(user: User): Promise<User> {
this.users.set(user.id, user)
return user
}
async findById(id: string): Promise<User | null> {
return this.users.get(id) || null
}
}
it('creates and retrieves user', async () => {
const fakeDb = new FakeDatabase()
const repo = new UserRepository(fakeDb)
await repo.create({ id: '123', email: 'test@example.com' })
const user = await repo.findById('123')
expect(user?.email).toBe('test@example.com')
})
Benefits:
- More realistic than mocks
- Tests actual logic flow
- Fast (in-memory)
Spy
Definition: Wraps real object to record calls while preserving real behavior
When to use: When you want real behavior but also need to verify calls
Example:
import { vi } from 'vitest'
it('logs errors when payment fails', async () => {
const logger = new Logger()
const loggerSpy = vi.spyOn(logger, 'error')
const service = new PaymentService(logger)
await service.charge(100, 'invalid_token')
expect(loggerSpy).toHaveBeenCalledWith(
'Payment failed',
expect.objectContaining({ token: 'invalid_token' })
)
})
Mocking Patterns
Pattern 1: Dependency Injection
Principle: Pass dependencies as constructor/function arguments
✓ Good (testable):
class UserService:
def __init__(self, db, email_client):
self.db = db
self.email_client = email_client
def register(self, email, password):
user = self.db.insert_user(email, password)
self.email_client.send_welcome(email)
return user
def test_register():
mock_db = Mock()
mock_email = Mock()
service = UserService(db=mock_db, email_client=mock_email)
service.register('test@example.com', 'password')
mock_db.insert_user.assert_called_once()
mock_email.send_welcome.assert_called_once()
❌ Bad (hard to test):
class UserService:
def __init__(self):
self.db = Database()
self.email_client = EmailClient()
def register(self, email, password):
user = self.db.insert_user(email, password)
self.email_client.send_welcome(email)
return user
Pattern 2: Interface/Protocol-Based Mocking
Principle: Define interfaces, mock implementations
Example (TypeScript):
interface PaymentGateway {
charge(amount: number, token: string): Promise<ChargeResult>
}
class StripeGateway implements PaymentGateway {
async charge(amount: number, token: string): Promise<ChargeResult> {
}
}
class MockPaymentGateway implements PaymentGateway {
async charge(amount: number, token: string): Promise<ChargeResult> {
return { id: 'mock_charge', status: 'succeeded' }
}
}
class PaymentService {
constructor(private gateway: PaymentGateway) {}
async processPayment(amount: number, token: string) {
const result = await this.gateway.charge(amount, token)
return result
}
}
it('processes payment', async () => {
const mockGateway = new MockPaymentGateway()
const service = new PaymentService(mockGateway)
const result = await service.processPayment(100, 'tok_visa')
expect(result.status).toBe('succeeded')
})
Pattern 3: Builder Pattern for Test Data
Principle: Use builders to create test objects with sensible defaults
Example:
class UserBuilder:
def __init__(self):
self.id = 'default-id'
self.email = 'default@example.com'
self.is_active = True
def with_id(self, id):
self.id = id
return self
def with_email(self, email):
self.email = email
return self
def inactive(self):
self.is_active = False
return self
def build(self):
return User(id=self.id, email=self.email, is_active=self.is_active)
def test_send_email_to_active_users():
active_user = UserBuilder().with_email('active@example.com').build()
inactive_user = UserBuilder().inactive().build()
email_service.send_newsletter([active_user, inactive_user])
assert_email_sent_to('active@example.com')
assert_email_not_sent_to('inactive@example.com')
Anti-Patterns to Avoid
Anti-Pattern 1: Mocking Everything
Problem: Tests become brittle, don't test real behavior
❌ Wrong:
def test_process_order():
mock_validate = Mock(return_value=True)
mock_calculate = Mock(return_value=100)
mock_save = Mock()
mock_email = Mock()
with patch.multiple('myapp.order',
validate=mock_validate,
calculate_total=mock_calculate,
save_order=mock_save,
send_confirmation=mock_email):
process_order(order_data)
✓ Better:
def test_process_order():
mock_db = Mock()
mock_email = Mock()
service = OrderService(db=mock_db, email=mock_email)
result = service.process_order(order_data)
assert result.total == 100
mock_db.save.assert_called_once()
mock_email.send.assert_called_once()
Anti-Pattern 2: Overly Specific Mocks
Problem: Tests break when irrelevant details change
❌ Brittle:
mock_api.post.assert_called_once_with(
'/users',
headers={'Content-Type': 'application/json', 'User-Agent': 'MyApp/1.0'},
json={'email': 'test@example.com', 'name': 'Test', 'age': 25},
timeout=30
)
✓ Resilient:
mock_api.post.assert_called_once()
call_args = mock_api.post.call_args
assert call_args[0][0] == '/users'
assert call_args[1]['json']['email'] == 'test@example.com'
Anti-Pattern 3: Testing Mock Configuration
Problem: Test verifies mock setup, not actual logic
❌ Wrong:
def test_mock_returns_correct_value():
mock_service = Mock()
mock_service.get_user.return_value = User(id='123')
assert mock_service.get_user().id == '123'
Anti-Pattern 4: Not Resetting Mocks Between Tests
Problem: Test pollution - tests affect each other
❌ Wrong:
mock_db = Mock()
def test_create_user():
service = UserService(db=mock_db)
service.create_user('test@example.com')
assert mock_db.insert.call_count == 1
def test_create_another_user():
service = UserService(db=mock_db)
service.create_user('test2@example.com')
assert mock_db.insert.call_count == 1
✓ Correct:
import pytest
@pytest.fixture
def mock_db():
return Mock()
def test_create_user(mock_db):
service = UserService(db=mock_db)
service.create_user('test@example.com')
assert mock_db.insert.call_count == 1
Language-Specific Best Practices
Python (pytest + unittest.mock)
from unittest.mock import Mock, patch, MagicMock
import pytest
@pytest.fixture
def mock_db():
return Mock()
def test_with_patching():
with patch('myapp.utils.send_email') as mock_email:
send_welcome_email('test@example.com')
mock_email.assert_called_once()
def test_context_manager():
mock_file = MagicMock()
with mock_file as f:
f.read.return_value = 'content'
TypeScript/JavaScript (Vitest/Jest)
import { describe, it, expect, vi, beforeEach } from 'vitest'
describe('UserService', () => {
let mockDb: any
beforeEach(() => {
mockDb = {
insert: vi.fn(),
findById: vi.fn()
}
})
it('creates user', async () => {
mockDb.insert.mockResolvedValue({ id: '123' })
const service = new UserService(mockDb)
const user = await service.create({ email: 'test@example.com' })
expect(user.id).toBe('123')
expect(mockDb.insert).toHaveBeenCalledOnce()
})
})
Summary Checklist
Before adding a mock to your test, ask:
Golden Rules:
- Mock at boundaries (I/O), not internals
- Prefer fakes over mocks when possible
- Use dependency injection
- Keep mocks simple and resilient
- Integration tests use real dependencies