Use mocking and test doubles to isolate code under test from external dependencies...
Mock external dependencies to create fast, isolated, and deterministic tests. This skill covers mocking strategies applicable across testing frameworks and programming languages.
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!'
)
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'
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)
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);
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);
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
}
}
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();
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()
Dependency is slow
Dependency is non-deterministic
Dependency is expensive
Dependency is unavailable
Testing error conditions
Testing integration points
Dependency is simple and fast
Mock would be as complex as real implementation
You're testing the thing you'd mock
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
}
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();
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()
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)
}
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
β 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' });
β 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);
β 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);
})
};
JavaScript:
beforeEach(() => {
jest.clearAllMocks();
});
Python:
def setup_method(self):
self.mock_db = Mock()
β 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'
};
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
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
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
Ask yourself:
Is it an external dependency?
Is it slow or expensive?
Is it the code under test?
Does mocking simplify the test?
Am I testing integration?