Test Mocking
Mock external dependencies to create fast, isolated, and deterministic tests. This skill covers mocking strategies applicable across testing frameworks and programming languages.
When to Use This Skill
- •User asks to "mock dependencies", "create mocks", "use test doubles"
- •Testing code with external dependencies (databases, APIs, cloud services)
- •Creating unit tests that run without credentials
- •Avoiding slow or expensive resource creation in tests
- •Isolating code under test from infrastructure dependencies
- •Making tests deterministic and reproducible
- •Testing error handling without triggering real errors
Types of Test Doubles
1. Mock
Definition: Object that verifies behavior (interactions)
Use when: You need to verify a method was called with specific arguments
Example (JavaScript/Jest):
const emailService = {
send: jest.fn()
};
sendWelcomeEmail(user, emailService);
expect(emailService.send).toHaveBeenCalledWith({
to: user.email,
subject: 'Welcome!'
});
Example (Python/unittest.mock):
from unittest.mock import Mock
email_service = Mock()
send_welcome_email(user, email_service)
email_service.send.assert_called_once_with(
to=user.email,
subject='Welcome!'
)
2. Stub
Definition: Object that provides canned responses
Use when: You need to control return values
Example (JavaScript):
const database = {
getUser: jest.fn().mockResolvedValue({ id: 1, name: 'John' })
};
const user = await fetchUserProfile(1, database);
expect(user.name).toBe('John');
Example (Python):
database = Mock()
database.get_user.return_value = {'id': 1, 'name': 'John'}
user = fetch_user_profile(1, database)
assert user['name'] == 'John'
3. Fake
Definition: Working implementation with shortcuts
Use when: You need realistic behavior without full complexity
Example (In-Memory Database):
class FakeUserRepository {
constructor() {
this.users = new Map();
}
save(user) {
this.users.set(user.id, user);
return Promise.resolve(user);
}
findById(id) {
return Promise.resolve(this.users.get(id));
}
}
// Use in tests
const repo = new FakeUserRepository();
await repo.save({ id: 1, name: 'John' });
const user = await repo.findById(1);
Example (Python):
class FakeUserRepository:
def __init__(self):
self.users = {}
def save(self, user):
self.users[user.id] = user
return user
def find_by_id(self, user_id):
return self.users.get(user_id)
4. Spy
Definition: Records information about calls while delegating to real object
Use when: You want real behavior + verification
Example (JavaScript):
const realService = {
processPayment: (amount) => ({ success: true, amount })
};
const spy = jest.spyOn(realService, 'processPayment');
const result = realService.processPayment(100);
expect(result.success).toBe(true);
expect(spy).toHaveBeenCalledWith(100);
Mocking Strategies
Strategy 1: Dependency Injection
Principle: Pass dependencies as parameters instead of creating them internally
❌ Hard to test (dependencies hardcoded):
function sendWelcomeEmail(user) {
const emailService = new EmailService(); // Hard-coded dependency
emailService.send({
to: user.email,
subject: 'Welcome!'
});
}
✅ Easy to test (dependency injected):
function sendWelcomeEmail(user, emailService) {
emailService.send({
to: user.email,
subject: 'Welcome!'
});
}
// In test
const mockEmailService = { send: jest.fn() };
sendWelcomeEmail(user, mockEmailService);
Strategy 2: Interface-Based Mocking
Principle: Depend on interfaces/protocols, not concrete implementations
Example (TypeScript):
interface EmailService {
send(message: EmailMessage): Promise<void>;
}
class RealEmailService implements EmailService {
async send(message: EmailMessage) {
// Real SMTP implementation
}
}
class MockEmailService implements EmailService {
async send(message: EmailMessage) {
// Mock implementation for testing
}
}
Strategy 3: Environment-Based Mocking
Principle: Use environment detection to swap implementations
Example (Terraform - Mock Provider):
mock_provider "aws" {}
run "test_without_credentials" {
# Provider mocked - no AWS calls
}
Example (JavaScript - Environment Check):
const emailService = process.env.NODE_ENV === 'test' ? new MockEmailService() : new RealEmailService();
Strategy 4: Module/Import Mocking
Principle: Replace entire modules during test execution
Example (Jest):
// In test file
jest.mock('./emailService', () => ({
sendEmail: jest.fn()
}));
const { sendEmail } = require('./emailService');
// sendEmail is now a mock function
Example (Python unittest.mock):
from unittest.mock import patch
@patch('myapp.email_service.send_email')
def test_send_welcome_email(mock_send):
send_welcome_email(user)
mock_send.assert_called_once()
When to Mock vs When to Use Real Implementation
✅ Mock When:
- •
Dependency is slow
- •Database queries
- •Network requests
- •File system operations
- •Cloud provider API calls
- •
Dependency is non-deterministic
- •Random number generators
- •Current time/date
- •External API responses
- •
Dependency is expensive
- •Cloud resource creation (EC2, S3, etc.)
- •Payment processing
- •SMS/email sending
- •
Dependency is unavailable
- •No test credentials
- •Service not in test environment
- •Rate-limited APIs
- •
Testing error conditions
- •Network timeouts
- •Service failures
- •Invalid responses
❌ Don't Mock When:
- •
Testing integration points
- •Verifying actual service behavior
- •End-to-end tests
- •Contract testing
- •
Dependency is simple and fast
- •Pure functions
- •Simple calculations
- •In-memory operations
- •
Mock would be as complex as real implementation
- •Over-mocking leads to brittle tests
- •Test the real behavior instead
- •
You're testing the thing you'd mock
- •Don't mock the system under test
- •Only mock dependencies
Framework-Specific Examples
Terraform Mock Provider
Basic Mock:
mock_provider "aws" {}
run "unit_test" {
command = plan
assert {
condition = aws_s3_bucket.bucket.bucket == "expected-name"
error_message = "Naming logic incorrect"
}
}
Mock with Defaults:
mock_provider "aws" {
mock_resource "aws_s3_bucket" {
defaults = {
arn = "arn:aws:s3:::test-bucket"
}
}
}
Override Specific Resources:
override_resource {
target = aws_rds_instance.database
values = {
endpoint = "mock-db.us-east-1.rds.amazonaws.com:5432"
}
}
run "test_without_rds" {
# RDS not created, uses override values
}
JavaScript/Jest Mocking
Mock Function:
const mockFn = jest.fn(); mockFn.mockReturnValue(42); expect(mockFn()).toBe(42); expect(mockFn).toHaveBeenCalled();
Mock Module:
jest.mock('./api', () => ({
fetchUser: jest.fn().mockResolvedValue({ id: 1, name: 'John' })
}));
Spy on Method:
const spy = jest.spyOn(object, 'method');
spy.mockImplementation(() => 'mocked value');
expect(object.method()).toBe('mocked value');
expect(spy).toHaveBeenCalled();
Python Mock (unittest.mock)
Mock Object:
from unittest.mock import Mock
mock_db = Mock()
mock_db.get_user.return_value = {'id': 1, 'name': 'John'}
user = service.fetch_user(1, mock_db)
mock_db.get_user.assert_called_once_with(1)
Patch Decorator:
from unittest.mock import patch
@patch('myapp.database.connection')
def test_fetch_user(mock_connection):
mock_connection.query.return_value = [{'id': 1}]
result = fetch_user(1)
assert result['id'] == 1
Context Manager:
from unittest.mock import patch
def test_send_email():
with patch('myapp.email.send') as mock_send:
send_welcome_email(user)
mock_send.assert_called_once()
Go Mocking (Interfaces)
Define Interface:
type UserRepository interface {
GetUser(id int) (*User, error)
SaveUser(user *User) error
}
Mock Implementation:
type MockUserRepository struct {
GetUserFunc func(id int) (*User, error)
SaveUserFunc func(user *User) error
}
func (m *MockUserRepository) GetUser(id int) (*User, error) {
return m.GetUserFunc(id)
}
func (m *MockUserRepository) SaveUser(user *User) error {
return m.SaveUserFunc(user)
}
Use in Test:
func TestFetchUserProfile(t *testing.T) {
mockRepo := &MockUserRepository{
GetUserFunc: func(id int) (*User, error) {
return &User{ID: 1, Name: "John"}, nil
},
}
user, err := FetchUserProfile(1, mockRepo)
assert.NoError(t, err)
assert.Equal(t, "John", user.Name)
}
Mock Data Management
Shared Mock Data Files
Terraform:
# tests/mocks/aws.tfmock.hcl
mock_resource "aws_s3_bucket" {
defaults = {
arn = "arn:aws:s3:::test-bucket"
}
}
mock_data "aws_ami" {
defaults = {
id = "ami-12345678"
}
}
# tests/unit.tftest.hcl
mock_provider "aws" {
source = "./tests/mocks"
}
JavaScript:
// tests/fixtures/users.js
export const mockUsers = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
];
// In test
import { mockUsers } from './fixtures/users';
database.getUsers.mockResolvedValue(mockUsers);
Python:
# tests/fixtures/users.py
MOCK_USERS = [
{'id': 1, 'name': 'John', 'email': 'john@example.com'},
{'id': 2, 'name': 'Jane', 'email': 'jane@example.com'}
]
# In test
from tests.fixtures.users import MOCK_USERS
mock_db.get_users.return_value = MOCK_USERS
Best Practices
1. Mock at the Boundaries
✅ Good - Mock external services:
// Mock HTTP client
const mockHttp = {
get: jest.fn().mockResolvedValue({ data: { user: 'John' } })
};
// Test your service
const result = await userService.fetchUser(1, mockHttp);
❌ Bad - Mock internal logic:
// Don't mock the thing you're testing
jest.mock('./userService');
const userService = require('./userService');
userService.fetchUser.mockResolvedValue({ user: 'John' });
2. Verify Behavior, Not Implementation
✅ Good - Test outcomes:
await sendWelcomeEmail(user, emailService);
expect(emailService.send).toHaveBeenCalledWith(
expect.objectContaining({
to: user.email,
subject: expect.stringContaining('Welcome')
})
);
❌ Bad - Test implementation details:
// Too brittle - breaks when implementation changes expect(emailService.send).toHaveBeenCalledTimes(1); expect(emailService.send.mock.calls[0][0].to).toBe(user.email);
3. Keep Mocks Simple
✅ Good - Simple, focused mock:
const mockDatabase = {
getUser: jest.fn().mockResolvedValue({ id: 1, name: 'John' })
};
❌ Bad - Complex mock duplicating real implementation:
const mockDatabase = {
getUser: jest.fn(async (id) => {
// Don't reimplement the real database logic
const users = await loadFromFile();
return users.find(u => u.id === id);
})
};
4. Reset Mocks Between Tests
JavaScript:
beforeEach(() => {
jest.clearAllMocks();
});
Python:
def setup_method(self):
self.mock_db = Mock()
5. Use Realistic Test Data
✅ Good - Realistic data:
const mockUser = {
id: 'usr_1234567890',
email: 'john.doe@example.com',
created_at: '2024-01-15T10:30:00Z'
};
❌ Bad - Unrealistic data:
const mockUser = {
id: 1,
email: 'test',
created_at: 'now'
};
Anti-Patterns
❌ Over-Mocking
Problem: Mocking everything, including simple logic
// Don't do this
jest.mock('./utils/add');
const add = require('./utils/add');
add.mockReturnValue(5);
expect(add(2, 3)).toBe(5);
// You're testing the mock, not the code!
Solution: Only mock external dependencies
❌ Leaky Mocks
Problem: Mock state persists between tests
describe('User tests', () => {
const mockDb = { getUser: jest.fn() };
test('test 1', () => {
mockDb.getUser.mockResolvedValue({ id: 1 });
});
test('test 2', () => {
// Mock still has state from test 1!
});
});
Solution: Reset mocks in beforeEach
❌ Mocking the System Under Test
Problem: Mocking the code you're trying to test
// Don't do this
jest.mock('./orderService');
const orderService = require('./orderService');
orderService.createOrder.mockResolvedValue({ id: 1 });
// You're not testing anything!
const result = await orderService.createOrder();
Solution: Only mock dependencies, test real implementation
Mocking Decision Tree
Ask yourself:
- •
Is it an external dependency?
- •✅ Yes → Consider mocking
- •❌ No → Don't mock
- •
Is it slow or expensive?
- •✅ Yes → Mock for unit tests
- •❌ No → Use real implementation
- •
Is it the code under test?
- •✅ Yes → Never mock
- •❌ No → Can mock
- •
Does mocking simplify the test?
- •✅ Yes → Mock
- •❌ No → Keep it real
- •
Am I testing integration?
- •✅ Yes → Don't mock
- •❌ No → Can mock
References
- •Test Doubles: https://martinfowler.com/bliki/TestDouble.html
- •Mocking Best Practices: https://kentcdodds.com/blog/mocking-is-a-code-smell
- •Terraform Mocking: https://developer.hashicorp.com/terraform/language/tests/mocking
- •Jest Mocking: https://jestjs.io/docs/mock-functions
- •Python unittest.mock: https://docs.python.org/3/library/unittest.mock.html
- •Test Assertions: Use the test-assertions skill for validation patterns
- •Test Organization: Use the test-organization-patterns skill for test structure