Test Doubles
What Are Test Doubles?
Test doubles replace real dependencies in tests. They let you control inputs and isolate the code you're testing.
Common types:
- Stub: Returns predefined responses
- Mock: Verifies interactions (method calls)
- Spy: Wraps real object to track calls
- Fake: Simplified working implementation
Terminology in Suites
Suites uses specific terminology:
Mock: A complete replacement of a dependency class where each method becomes a stub.
const repo: Mocked<UserRepository> = unitRef.get(UserRepository);
// All methods are stubs you can configure
Stub: An individual method that returns predefined responses.
repo.findById.mockResolvedValue(testUser); // This method is a stub
When you see Mocked<UserRepository>, it means a class where all methods are stubs.
Two Testing Approaches
Suites supports both state and behavior verification (as described in Martin Fowler's "Mocks Aren't Stubs"):
State Verification (with stubs): Test the outcome, not how you got there.
repo.findById.mockResolvedValue(testUser);
const result = await service.getUserName(1);
expect(result).toBe('John Doe'); // Verify state
Behavior Verification (with spies): Test the interactions between objects.
await service.createUser(userData);
expect(repo.save).toHaveBeenCalledWith(userData); // Verify interaction
Both work with Suites. Choose based on what you're testing.
Why spyOn() Is Problematic
jest.spyOn(object, 'method') wraps a method to track calls. By default, it calls the real implementation.
The problem:
// ❌ Want to verify validateUser is called
jest.spyOn(service, 'validateUser');
await service.createUser(data);
expect(service.validateUser).toHaveBeenCalled();
// But validateUser RUNS FOR REAL!
// - Might have side effects
// - Might throw errors
// - Might have its own dependencies
// - Test becomes fragile
Why this happens:
Developers use spyOn to track calls, but forget it executes real code unless you add .mockImplementation(). This leads to:
- Unexpected side effects in tests
- Tests breaking when implementation changes
- Confusion about what's real vs mocked
Suites alternative - Sociable tests:
// ✅ Test REAL business logic interactions properly
const { unit } = await TestBed.sociable(UserService)
.expose(UserValidator) // Real validator
.compile();
await unit.createUser(data);
// Real validation runs in controlled test environment
// Token-injected I/O still mocked
// Tests actual business logic interaction
Or behavior verification on mocks:
// ✅ Verify calls on auto-mocked dependencies
const { unit, unitRef } = await TestBed.solitary(UserService).compile();
const validator = unitRef.get(UserValidator); // Mocked
await unit.createUser(data);
expect(validator.validate).toHaveBeenCalledWith(data); // Safe - it's a mock
Why sociable is better than spyOn:
- Sociable: Real business logic runs, I/O mocked via tokens, controlled
- spyOn: Real method runs, unclear what else runs, uncontrolled
- Sociable: Intentional testing of real interactions
- spyOn: Accidental execution of real code
See Mocks Aren't Stubs for deeper discussion on state vs behavior verification trade-offs.
Using Mocks in Suites
Suites automatically generates mocks for all dependencies:
const { unit, unitRef } = await TestBed.solitary(UserService).compile();
const repo = unitRef.get(UserRepository); // Auto-generated mock
// Configure stub responses
repo.findById.mockResolvedValue(testUser);
For configuration options, see:
- Mock Configuration -
.mock().final()and.mock().impl() - Types -
Mocked<T>type details - mock() function - Creating standalone mocks
Testing Interactions
To test how components work together, use sociable tests with real implementations:
const { unit } = await TestBed.sociable(UserService)
.expose(UserRepository) // Real implementation
.compile();
// Test real interaction between UserService and UserRepository
const result = await unit.createUser(userData);
expect(result.id).toBeDefined(); // Real logic executed
See Sociable Unit Tests for details.
Next Steps
- Solitary Unit Tests - Testing in complete isolation
- Sociable Unit Tests - Testing with real dependencies
- API Reference - Technical details