- Authors

- Name
- Youngju Kim
- @fjvbn20031
Overview
When writing software tests, the most commonly confused concepts are Stub, Mock, Spy, and Fake. These terms are often used interchangeably, but according to the Test Double taxonomy established by Gerard Meszaros in his book "xUnit Test Patterns," each plays a distinctly different role.
This guide covers the precise meaning of each test double, when to use them, language-specific implementation examples, and testing strategies including TDD and BDD.
1. The 5 Types of Test Doubles
A Test Double is a term borrowed from film production, where a stunt double steps in for dangerous scenes. In software, a test double is any object that replaces a real dependency to make testing possible.
Gerard Meszaros's Taxonomy
Test Double
├── Dummy Object (passed but never used)
├── Test Stub (returns canned answers - state verification)
├── Test Spy (records how it was called)
├── Mock Object (pre-programmed with expectations - behavior verification)
└── Fake Object (working implementation unsuitable for production)
1.1 Dummy Object
A Dummy is passed to satisfy method signatures but is never actually used during the test.
// Example: Logger is required but not relevant to the test
class DummyLogger implements Logger {
log(message: string): void {}
error(message: string): void {}
warn(message: string): void {}
}
test('user creation - email validation', () => {
const dummyLogger = new DummyLogger()
const userService = new UserService(dummyLogger)
expect(userService.validateEmail('test@example.com')).toBe(true)
})
Dummies are used purely to avoid null pointer exceptions or compilation errors.
1.2 Stub
A Stub returns canned (pre-programmed) answers to calls. It only returns what you set up and performs no other behavior. Stubs are used for State Verification.
// UserRepository Stub
class StubUserRepository implements UserRepository {
findById(id: string): User | null {
if (id === 'user-123') {
return { id: 'user-123', name: 'John Doe', email: 'john@example.com' }
}
return null
}
save(user: User): void {}
}
test('user retrieval service', () => {
const stub = new StubUserRepository()
const service = new UserService(stub)
const user = service.getUser('user-123')
expect(user?.name).toBe('John Doe')
})
1.3 Spy
A Spy wraps a real implementation or acts like a Stub, while also recording call information so you can verify later how it was called.
class SpyEmailService implements EmailService {
public sentEmails: Array<{ to: string; subject: string }> = []
sendEmail(to: string, subject: string, body: string): void {
this.sentEmails.push({ to, subject })
// No actual sending
}
}
test('email sent on order completion', () => {
const spyEmail = new SpyEmailService()
const orderService = new OrderService(spyEmail)
orderService.completeOrder('order-456')
expect(spyEmail.sentEmails).toHaveLength(1)
expect(spyEmail.sentEmails[0].to).toBe('customer@example.com')
})
1.4 Mock Object
A Mock is an object with pre-programmed expectations. After the test executes, the mock automatically verifies that it was called as expected. Mocks are used for Behavior Verification.
While a Spy records and you assert after the fact, a Mock pre-declares what it expects and fails immediately if the expectation is not met.
// Jest Mock example
test('email sent exactly once on order completion', () => {
const mockEmailService = {
sendEmail: jest.fn(),
}
const orderService = new OrderService(mockEmailService)
orderService.completeOrder('order-456')
expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(1)
expect(mockEmailService.sendEmail).toHaveBeenCalledWith(
'customer@example.com',
'Order Complete',
expect.stringContaining('order-456')
)
})
1.5 Fake Object
A Fake has a working implementation but is not suitable for production. Examples include in-memory databases, in-memory queues, and file-based storage.
class FakeUserRepository implements UserRepository {
private store: Map<string, User> = new Map()
findById(id: string): User | null {
return this.store.get(id) ?? null
}
save(user: User): void {
this.store.set(user.id, user)
}
findByEmail(email: string): User | null {
for (const user of this.store.values()) {
if (user.email === email) return user
}
return null
}
}
test('prevent duplicate email registration', () => {
const fake = new FakeUserRepository()
const service = new UserService(fake)
service.register({ id: '1', name: 'John Doe', email: 'test@example.com' })
expect(() => service.register({ id: '2', name: 'Jane Doe', email: 'test@example.com' })).toThrow(
'Email is already in use'
)
})
2. Stub In Depth
2.1 When to Use a Stub
Stubs are valuable in these situations:
- Depends on an external service (payment API, SMS provider)
- Returns non-deterministic results (current time, random values)
- Slow dependencies (database queries, network calls)
- Reproducing specific error conditions
2.2 In-Memory Repository Stub Pattern
// Go language example
package repository
type UserRepository interface {
FindByID(id string) (*User, error)
Save(user *User) error
}
// Test Stub
type StubUserRepository struct {
users map[string]*User
err error
}
func NewStubUserRepository() *StubUserRepository {
return &StubUserRepository{
users: make(map[string]*User),
}
}
func (s *StubUserRepository) FindByID(id string) (*User, error) {
if s.err != nil {
return nil, s.err
}
user, ok := s.users[id]
if !ok {
return nil, ErrUserNotFound
}
return user, nil
}
func (s *StubUserRepository) WithError(err error) *StubUserRepository {
s.err = err
return s
}
func (s *StubUserRepository) AddUser(user *User) *StubUserRepository {
s.users[user.ID] = user
return s
}
2.3 Time Stub (Clock Interface)
Code that depends on time is notoriously hard to test. Abstract it behind a Clock interface.
// Clock interface
type Clock interface {
Now() time.Time
}
// Production implementation
type SystemClock struct{}
func (c *SystemClock) Now() time.Time {
return time.Now()
}
// Test Stub
type StubClock struct {
fixedTime time.Time
}
func NewStubClock(t time.Time) *StubClock {
return &StubClock{fixedTime: t}
}
func (c *StubClock) Now() time.Time {
return c.fixedTime
}
// Usage
func TestTokenExpiry(t *testing.T) {
fixedTime := time.Date(2026, 3, 17, 12, 0, 0, 0, time.UTC)
stubClock := NewStubClock(fixedTime)
tokenService := NewTokenService(stubClock)
token := tokenService.CreateToken("user-123")
// Check expiry 2 hours later
expiredClock := NewStubClock(fixedTime.Add(2 * time.Hour))
tokenService2 := NewTokenService(expiredClock)
if tokenService2.IsValid(token) {
t.Error("expired token considered valid")
}
}
2.4 External API Stub (TypeScript)
// Payment gateway interface
interface PaymentGateway {
charge(amount: number, cardToken: string): Promise<PaymentResult>
refund(paymentId: string): Promise<RefundResult>
}
// Test Stub
class StubPaymentGateway implements PaymentGateway {
private shouldFail = false
setToFail(): void {
this.shouldFail = true
}
async charge(amount: number, cardToken: string): Promise<PaymentResult> {
if (this.shouldFail) {
return { success: false, error: 'CARD_DECLINED' }
}
return { success: true, paymentId: `PAY-${Date.now()}` }
}
async refund(paymentId: string): Promise<RefundResult> {
return { success: true, refundId: `REF-${paymentId}` }
}
}
// Java (Mockito) example
// @ExtendWith(MockitoExtension.class)
// class OrderServiceTest {
// @Mock
// PaymentGateway paymentGateway;
//
// @Test
// void should_cancel_order_on_payment_failure() {
// when(paymentGateway.charge(anyDouble(), anyString()))
// .thenReturn(new PaymentResult(false, "CARD_DECLINED"));
// ...
// }
// }
3. Mock In Depth
3.1 Mock vs Stub: The Key Distinction
This distinction is crucial. Martin Fowler's article "Mocks Aren't Stubs" clearly separates the two:
| Aspect | Stub | Mock |
|---|---|---|
| Verification type | State Verification | Behavior Verification |
| Assertion target | State of the system under test | Calls made on the double |
| Expectation setup | Returns values | Pre-declares expected calls |
| Failure timing | After test assertions | On expectation violation |
// Stub approach - state verification
test('order completion state (Stub)', async () => {
const stubPayment = new StubPaymentGateway()
const orderService = new OrderService(stubPayment)
const order = await orderService.placeOrder(items, cardToken)
// Verify the state of the returned object
expect(order.status).toBe('COMPLETED')
expect(order.paymentId).toBeDefined()
})
// Mock approach - behavior verification
test('payment API called on order completion (Mock)', async () => {
const mockPayment = { charge: jest.fn().mockResolvedValue({ success: true, paymentId: 'PAY-1' }) }
const orderService = new OrderService(mockPayment)
await orderService.placeOrder(items, 'card-token')
// Verify that the payment API was called with correct arguments
expect(mockPayment.charge).toHaveBeenCalledWith(totalAmount, 'card-token')
})
3.2 The Danger of Over-Mocking
Overusing Mocks couples your tests to implementation details. If internal implementation changes while behavior stays the same, tests break.
// Bad - over-verifying implementation internals
test('save user - excessive mocking', async () => {
const mockRepo = {
beginTransaction: jest.fn(),
save: jest.fn(),
commitTransaction: jest.fn(),
closeConnection: jest.fn(),
}
// Depends on internal call order - fragile under refactoring
expect(mockRepo.beginTransaction).toHaveBeenCalledBefore(mockRepo.save)
})
// Good - verify only meaningful behavior
test('user save succeeds', async () => {
const mockRepo = { save: jest.fn().mockResolvedValue(undefined) }
const service = new UserService(mockRepo)
await service.createUser({ name: 'John Doe', email: 'john@example.com' })
expect(mockRepo.save).toHaveBeenCalledTimes(1)
})
3.3 The "Don't Mock What You Don't Own" Principle
Never mock third-party libraries directly. Instead, create an adapter or wrapper interface that you control and mock that.
// Bad - mocking axios directly
jest.mock('axios')
test('API call', async () => {
;(axios.get as jest.Mock).mockResolvedValue({ data: { name: 'John Doe' } })
// ...
})
// Good - define an adapter and mock the adapter
interface HttpClient {
get<T>(url: string): Promise<T>
post<T>(url: string, data: unknown): Promise<T>
}
class AxiosHttpClient implements HttpClient {
async get<T>(url: string): Promise<T> {
const response = await axios.get<T>(url)
return response.data
}
async post<T>(url: string, data: unknown): Promise<T> {
const response = await axios.post<T>(url, data)
return response.data
}
}
// In tests, mock the HttpClient interface
4. The Spy Pattern
4.1 Spy vs Mock
| Aspect | Spy | Mock |
|---|---|---|
| Expectation setup | After-the-fact assertions | Pre-declared expectations |
| Real implementation | Optionally delegates to real | Always returns doubles |
| Use case | Watch real behavior | Full replacement |
4.2 Jest spyOn Example
// spyOn watches methods on real objects
class EmailService {
sendWelcomeEmail(user: User): void {
console.log(`Sending email to: ${user.email}`)
}
}
test('welcome email sent on registration', () => {
const emailService = new EmailService()
const spy = jest.spyOn(emailService, 'sendWelcomeEmail')
spy.mockImplementation(() => {}) // Prevent actual sending
const userService = new UserService(emailService)
userService.register({ name: 'John Doe', email: 'john@example.com' })
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(expect.objectContaining({ email: 'john@example.com' }))
spy.mockRestore() // Restore original implementation
})
4.3 Go Language Spy Pattern
type SpyNotificationService struct {
Calls []NotificationCall
delegate NotificationService
}
type NotificationCall struct {
UserID string
Message string
Type string
}
func (s *SpyNotificationService) Notify(userID, message, notifType string) error {
s.Calls = append(s.Calls, NotificationCall{
UserID: userID,
Message: message,
Type: notifType,
})
if s.delegate != nil {
return s.delegate.Notify(userID, message, notifType)
}
return nil
}
func (s *SpyNotificationService) WasCalledWith(userID, notifType string) bool {
for _, call := range s.Calls {
if call.UserID == userID && call.Type == notifType {
return true
}
}
return false
}
func TestOrderCompletion_SendsNotification(t *testing.T) {
spy := &SpyNotificationService{}
service := NewOrderService(spy)
service.CompleteOrder("user-123", "order-456")
if !spy.WasCalledWith("user-123", "ORDER_COMPLETE") {
t.Error("order completion notification not sent")
}
}
4.4 Java Mockito @Spy
// @Spy observes a real object
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Spy
EmailService emailService = new EmailService();
@InjectMocks
UserService userService;
@Test
void should_send_welcome_email_on_registration() {
userService.register(new User("John Doe", "john@example.com"));
verify(emailService, times(1)).sendWelcomeEmail(
argThat(user -> user.getEmail().equals("john@example.com"))
);
}
}
5. Fake Objects
5.1 InMemoryDatabase Fake Implementation
A Fake behaves like the real database but stores data in memory. This lets you run integration tests across multiple services without a real database.
// Go in-memory Fake Repository
type InMemoryUserRepository struct {
mu sync.RWMutex
store map[string]*User
seq int64
}
func NewInMemoryUserRepository() *InMemoryUserRepository {
return &InMemoryUserRepository{
store: make(map[string]*User),
}
}
func (r *InMemoryUserRepository) FindByID(id string) (*User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
user, ok := r.store[id]
if !ok {
return nil, ErrUserNotFound
}
copied := *user
return &copied, nil
}
func (r *InMemoryUserRepository) Save(user *User) error {
r.mu.Lock()
defer r.mu.Unlock()
if user.ID == "" {
r.seq++
user.ID = fmt.Sprintf("user-%d", r.seq)
}
copied := *user
r.store[user.ID] = &copied
return nil
}
func (r *InMemoryUserRepository) FindByEmail(email string) (*User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, user := range r.store {
if user.Email == email {
copied := *user
return &copied, nil
}
}
return nil, ErrUserNotFound
}
func (r *InMemoryUserRepository) Reset() {
r.mu.Lock()
defer r.mu.Unlock()
r.store = make(map[string]*User)
r.seq = 0
}
5.2 Fake EmailService Implementation
class FakeEmailService implements EmailService {
private emails: Email[] = []
async send(email: Email): Promise<void> {
this.emails.push({ ...email, sentAt: new Date() })
}
// Test helper methods
getEmailsSentTo(address: string): Email[] {
return this.emails.filter((e) => e.to === address)
}
getLastEmail(): Email | undefined {
return this.emails[this.emails.length - 1]
}
clear(): void {
this.emails = []
}
hasEmailWithSubject(subject: string): boolean {
return this.emails.some((e) => e.subject.includes(subject))
}
}
const fakeEmail = new FakeEmailService()
beforeEach(() => fakeEmail.clear())
test('welcome email sent on registration', async () => {
const service = new UserService(fakeEmail)
await service.register({ name: 'John Doe', email: 'john@example.com' })
const emails = fakeEmail.getEmailsSentTo('john@example.com')
expect(emails).toHaveLength(1)
expect(emails[0].subject).toBe('Welcome!')
})
test('password reset email sent', async () => {
const service = new UserService(fakeEmail)
await service.requestPasswordReset('john@example.com')
expect(fakeEmail.hasEmailWithSubject('Password Reset')).toBe(true)
})
5.3 Fake Clock Implementation
interface Clock {
now(): Date
advance(ms: number): void
}
class FakeClock implements Clock {
private currentTime: Date
constructor(initialTime?: Date) {
this.currentTime = initialTime ?? new Date('2026-03-17T09:00:00Z')
}
now(): Date {
return new Date(this.currentTime)
}
advance(ms: number): void {
this.currentTime = new Date(this.currentTime.getTime() + ms)
}
advanceBy(options: { hours?: number; minutes?: number; days?: number }): void {
const ms =
(options.hours ?? 0) * 3600000 +
(options.minutes ?? 0) * 60000 +
(options.days ?? 0) * 86400000
this.advance(ms)
}
}
test('session token expiry', () => {
const clock = new FakeClock()
const authService = new AuthService(clock)
const token = authService.createSession('user-123')
expect(authService.isSessionValid(token)).toBe(true)
// 25 hours later
clock.advanceBy({ hours: 25 })
expect(authService.isSessionValid(token)).toBe(false)
})
5.4 Fake vs Mock: When to Choose Which
Choose Fake when:
- The same dependency is reused across many tests
- The dependency has complex stateful behavior (e.g., a database)
- Interactions between tests matter
- You want tests that are resilient to refactoring
Choose Mock when:
- You need to precisely verify a specific call scenario
- You need to prevent side effects of external services
- You need quick setup in a simple unit test
6. Testing Strategies
6.1 The Testing Pyramid
The classic model proposed by Mike Cohn.
/\
/ \
/ E2E\ Few, expensive
/------\
/Integrat.\ Moderate
/------------\
/ Unit Tests \ Many, cheap
/--------------\
- Unit Tests: Fast and isolated. Test doubles used heavily.
- Integration Tests: Test interactions between components.
- E2E Tests: Full user journey tests.
6.2 The Testing Trophy (Kent C. Dodds)
Proposed by Kent C. Dodds, creator of React Testing Library.
/\
/e2e\
/------\
/integrat.\ <- Focus here
/------------\
/ unit tests \
/--------------\
/ static analysis\ <- Foundation
/------------------\
The philosophy is "write more integration tests" — minimize test doubles and test closer to real behavior.
"Write tests. Not too many. Mostly integration." - Kent C. Dodds
6.3 The Ice Cream Cone Anti-Pattern
/\
/ \
/ E2E \ Many - slow and fragile
/--------\
/Integrat. \
/------------\
/ Unit Tests \ Few - insufficient unit coverage
/----------------\
This anti-pattern over-relies on E2E tests: slow, brittle, and hard to debug.
6.4 The Honeycomb (Spotify Model)
Proposed by Spotify engineering for microservices environments.
Focus on integration tests at the center:
- Service integration tests (within a service)
- User journey tests (across services)
- Unit tests (business logic isolation)
Emphasizes service contracts and internal integration over excessive mocking.
7. TDD (Test-Driven Development)
7.1 Red-Green-Refactor Cycle
1. RED - Write a failing test
2. GREEN - Write the minimum code to make it pass
3. REFACTOR - Improve code quality (tests must stay green)
7.2 Outside-In TDD vs Inside-Out TDD
London School (Outside-In, Mockist)
- Start from the external interface, work inward
- Use Mocks extensively to define collaborators
- Top-down design driven by user scenarios
Chicago School (Inside-Out, Classicist)
- Start from the domain model, work outward
- Use real objects as much as possible, minimize Mocks
- Build stable unit tests first, then integrate
7.3 TDD Example in Go: FizzBuzz
// Step 1: Write failing tests (RED)
func TestFizzBuzz(t *testing.T) {
tests := []struct {
input int
expected string
}{
{1, "1"},
{3, "Fizz"},
{5, "Buzz"},
{15, "FizzBuzz"},
{9, "Fizz"},
{10, "Buzz"},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("input=%d", tt.input), func(t *testing.T) {
got := FizzBuzz(tt.input)
if got != tt.expected {
t.Errorf("FizzBuzz(%d) = %q, want %q", tt.input, got, tt.expected)
}
})
}
}
// Step 2: Minimal code to make tests pass (GREEN)
func FizzBuzz(n int) string {
switch {
case n%15 == 0:
return "FizzBuzz"
case n%3 == 0:
return "Fizz"
case n%5 == 0:
return "Buzz"
default:
return strconv.Itoa(n)
}
}
// Step 3: Refactor if needed (REFACTOR)
7.4 Shopping Cart TDD Example
func TestCart_AddItem(t *testing.T) {
cart := NewCart()
item := Item{ID: "book-1", Name: "Clean Code", Price: 2500}
cart.AddItem(item, 2)
if cart.TotalItems() != 2 {
t.Errorf("expected 2 items, got %d", cart.TotalItems())
}
if cart.TotalPrice() != 5000 {
t.Errorf("expected 5000, got %d", cart.TotalPrice())
}
}
func TestCart_ApplyDiscount(t *testing.T) {
cart := NewCart()
cart.AddItem(Item{ID: "item-1", Price: 10000}, 3)
cart.ApplyDiscount(0.1) // 10% discount
if cart.TotalPrice() != 27000 {
t.Errorf("expected 27000 after 10%% discount, got %d", cart.TotalPrice())
}
}
8. BDD (Behavior-Driven Development)
8.1 Given-When-Then Structure
BDD extends TDD from a business perspective. Tests are written in a near-natural-language format.
Given (preconditions): the initial context or setup
When (action): an action performed by the user or system
Then (outcome): the expected result
8.2 Cucumber/Gherkin Syntax
Feature: Shopping Cart Checkout
As a registered customer
I want to add items to my cart and pay
So that I can purchase products
Scenario: Successful checkout
Given my cart contains 1 copy of "Clean Code"
And a valid payment card is registered
When I click the checkout button
Then my order is completed
And an order confirmation email is sent
Scenario: Checkout fails due to out-of-stock
Given my cart contains 1 "Out of Stock Item"
When I click the checkout button
Then I see the error "Item is out of stock"
And no order is created
8.3 TypeScript BDD Style (Jest)
describe('Shopping Cart Checkout', () => {
describe('Successful checkout', () => {
it('should complete order and send confirmation email', async () => {
// Given
const fakeEmail = new FakeEmailService()
const fakePayment = new FakePaymentGateway()
const cart = new Cart()
cart.addItem({ id: 'book-1', name: 'Clean Code', price: 2500 })
const orderService = new OrderService(fakePayment, fakeEmail)
// When
const order = await orderService.checkout(cart, 'card-token-123')
// Then
expect(order.status).toBe('COMPLETED')
expect(fakeEmail.hasEmailWithSubject('Order Confirmation')).toBe(true)
})
})
describe('Out of stock', () => {
it('should fail with out-of-stock error', async () => {
// Given
const stubInventory = new StubInventoryService()
stubInventory.setOutOfStock('out-of-stock-item')
const cart = new Cart()
cart.addItem({ id: 'out-of-stock-item', price: 1000 })
const orderService = new OrderService(
new FakePaymentGateway(),
new FakeEmailService(),
stubInventory
)
// When & Then
await expect(orderService.checkout(cart, 'card-token')).rejects.toThrow(
'Item is out of stock'
)
})
})
})
8.4 Java JBehave Concept
// JBehave Story file: order_checkout.story
// Scenario: Successful checkout
//
// Given the cart contains 1 item
// When checkout is requested
// Then the order is completed
// Step implementation
public class OrderCheckoutSteps {
private Cart cart;
private Order completedOrder;
@Given("the cart contains 1 item")
public void givenCartWithOneItem() {
cart = new Cart();
cart.addItem(new Item("item-1", "Test Product", 1000));
}
@When("checkout is requested")
public void whenCheckoutRequested() {
completedOrder = orderService.checkout(cart, "test-card-token");
}
@Then("the order is completed")
public void thenOrderCompleted() {
assertThat(completedOrder.getStatus()).isEqualTo("COMPLETED");
}
}
9. Contract Testing
9.1 What Is Contract Testing?
In microservices architectures, Contract Testing verifies the API contracts between services without running all services simultaneously.
9.2 Pact.io - Consumer-Driven Contract Testing
// Consumer test (Order Service consuming User Service API)
import { PactV3, MatchersV3 } from '@pact-foundation/pact'
const provider = new PactV3({
consumer: 'OrderService',
provider: 'UserService',
dir: './pacts',
})
describe('UserService Contract', () => {
test('get user by ID', () => {
return provider
.addInteraction({
states: [{ description: 'user-123 exists' }],
uponReceiving: 'a request for user-123',
withRequest: {
method: 'GET',
path: '/users/user-123',
},
willRespondWith: {
status: 200,
body: {
id: MatchersV3.string('user-123'),
name: MatchersV3.string('John Doe'),
email: MatchersV3.email('john@example.com'),
},
},
})
.executeTest(async (mockServer) => {
const client = new UserServiceClient(mockServer.url)
const user = await client.getUser('user-123')
expect(user.id).toBe('user-123')
expect(user.name).toBe('John Doe')
})
})
})
9.3 Provider Verification
// The provider verifies the consumer's contract
import { Verifier } from '@pact-foundation/pact'
test('UserService satisfies OrderService contract', () => {
return new Verifier({
providerBaseUrl: 'http://localhost:3001',
pactUrls: ['./pacts/OrderService-UserService.json'],
stateHandlers: {
'user-123 exists': async () => {
await testDatabase.seed({ id: 'user-123', name: 'John Doe', email: 'john@example.com' })
},
},
}).verifyProvider()
})
10. Quiz
Quiz 1: What is the most important difference between a Stub and a Mock?
Answer: The type of verification. A Stub is used for State Verification, while a Mock is used for Behavior Verification.
Explanation: A Stub verifies the final state of the system under test (return values, property changes, etc.). A Mock verifies how a collaborator object (dependency) was called (call count, arguments, etc.). Martin Fowler's article "Mocks Aren't Stubs" clearly explains this distinction.
Quiz 2: What does the "Don't mock what you don't own" principle mean?
Answer: You should never directly mock external libraries or third-party code. Instead, create an adapter or wrapper interface that you own and mock that.
Explanation: Mocking external libraries directly ties your tests to the library's API. Any library upgrade that changes the API can break tests, and your mock may not accurately reflect the library's real behavior. Using the adapter pattern improves test stability and makes the dependency replaceable.
Quiz 3: What is the difference between a Fake and a Stub?
Answer: A Fake has a real, working simplified implementation, whereas a Stub returns pre-configured values.
Explanation: A FakeUserRepository actually performs CRUD operations using an in-memory map. A StubUserRepository just returns hardcoded values for specific IDs. Fakes are reusable across many tests and suited for dependencies with complex stateful behavior.
Quiz 4: What is the difference between the London School and Chicago School of TDD?
Answer: The London School (Mockist) starts from the external interface, using Mocks heavily for top-down design. The Chicago School (Classicist) starts from the internal domain model, using real objects for bottom-up design.
Explanation: The London School pre-defines collaborating objects with Mocks before they are implemented, driving design from the outside. The Chicago School builds a solid domain model first and integrates outward. Both are valid approaches used depending on context.
Quiz 5: What is the core argument of the Testing Trophy compared to the Testing Pyramid?
Answer: The Testing Trophy argues that integration tests should be the majority. Rather than accumulating unit tests, tests that are closer to real usage provide more value.
Explanation: Kent C. Dodds emphasizes "Write tests. Not too many. Mostly integration." Excessive use of Mocks in unit tests can lead to tests that don't actually guarantee real behavior. The Trophy places integration tests at the center to test closer to how the software is actually used.