What and Why
A test double is any object or function that replaces a real dependency in a test. The term comes from film: a stunt double stands in for an actor. In testing, a double stands in for a database, an API client, a clock, a queue — anything external that you want to control or observe.
You encounter test doubles whenever your code interacts with I/O. Without them, unit tests become integration tests: slow, unreliable, dependent on external systems being available.
Why they matter:
- Speed: a test that hits a real database takes 10–100ms. A test using a double takes microseconds. This compounds across hundreds of tests.
- Determinism: real external systems introduce variability (network latency, stale data, rate limits). Doubles return exactly what you configure.
- Edge case coverage: simulating a database timeout, a malformed API response, or a race condition is trivial with a double and nearly impossible with the real thing.
- Isolation: when a test fails, you know it's your code that's wrong — not a flaky downstream service.
A backend service fetches exchange rates from a third-party currency API. During unit testing, the team notices that tests occasionally fail because the API returns different values at different times, and sometimes the API is rate-limited. Which benefit of using a test double most directly addresses this problem?
Core Concepts
The Five Types of Test Doubles
These terms come from Gerard Meszaros' xUnit Test Patterns and are widely used (though many engineers use "mock" to mean all of them — a source of confusion):
Dummy — An object that is passed around but never used. Satisfies a parameter type requirement.
// UserService requires a Logger, but this test doesn't care about logging
logger := &DummyLogger{} // implements Logger interface, all methods are no-ops
svc := NewUserService(db, logger)Stub — Returns pre-configured responses. Used to feed controlled inputs to the unit under test.
type StubUserRepo struct{}
func (r *StubUserRepo) GetByID(id string) (*User, error) {
return &User{ID: id, Name: "Alice", Role: "admin"}, nil
}The stub doesn't verify how it was called — it just provides canned data.
Fake — A working, simplified implementation. Has real behavior but isn't production-grade (e.g., in-memory store instead of a database).
type InMemoryUserRepo struct {
users map[string]*User
}
func (r *InMemoryUserRepo) GetByID(id string) (*User, error) {
user, ok := r.users[id]
if !ok {
return nil, ErrNotFound
}
return user, nil
}
func (r *InMemoryUserRepo) Save(user *User) error {
r.users[user.ID] = user
return nil
}Fakes are more expensive to build but more versatile — they support testing state across multiple calls.
Spy — Records how it was called. You assert on the recording after the fact.
type SpyEmailSender struct {
sentEmails []Email
}
func (s *SpyEmailSender) Send(email Email) error {
s.sentEmails = append(s.sentEmails, email)
return nil
}
// In test:
spy := &SpyEmailSender{}
svc := NewNotificationService(spy)
svc.WelcomeUser("alice@example.com")
if len(spy.sentEmails) != 1 {
t.Errorf("expected 1 email, got %d", len(spy.sentEmails))
}
if spy.sentEmails[0].To != "alice@example.com" {
t.Errorf("expected email to alice, got %s", spy.sentEmails[0].To)
}Mock — A pre-configured object that also has built-in expectations. The mock will fail the test if its expectations aren't met. Mock libraries (like testify/mock in Go, Mockito in Java, unittest.mock in Python) provide these.
// Using testify/mock
mockRepo := new(MockUserRepo)
mockRepo.On("GetByID", "user-123").Return(&User{ID: "user-123"}, nil)
svc := NewUserService(mockRepo)
svc.GetProfile("user-123")
mockRepo.AssertExpectations(t) // fails if GetByID wasn't called with "user-123"Stubs vs. Mocks: The Key Distinction
Stubs control inputs to the unit under test. Mocks verify interactions (that the unit called its dependencies correctly).
Stub → used for state verification: "did the output have the right value?"
Mock → used for behavior verification: "did the code call the dependency correctly?"Overusing mocks leads to brittle tests. If you're mocking every layer and asserting on every internal call, you're testing the implementation, not the behavior. When you refactor, your tests break even if the observable behavior is unchanged.
Dependency Injection Enables Test Doubles
Substituting doubles is easy when the code accepts its dependencies from outside. Hard-coding dependencies (instantiating them inside functions) makes swapping hard. In Go you must change the code. In Python you must patch the name at runtime (for example with unittest.mock.patch).
// Hard to test — dependency is internal
func ProcessPayment(amount float64) error {
stripe := stripe.NewClient(os.Getenv("STRIPE_KEY")) // baked in
return stripe.Charge(amount)
}
// Testable — dependency is injected
type PaymentGateway interface {
Charge(amount float64) error
}
func ProcessPayment(gateway PaymentGateway, amount float64) error {
return gateway.Charge(amount)
}Now your test passes a StubPaymentGateway or MockPaymentGateway. The production caller passes the real Stripe client.
A fake keeps state across calls
The in-memory fake saves Alice and reads her back; a lookup for a missing ID returns ErrNotFound.
Saving Alice changes the map, so later calls can read her.
Text version
- Saving Alice changes the map, so later calls can read her.
- Looking up abc finds the user saved by the earlier call.
- The fake returns Alice.
- Looking up the ID missing finds no user, so the error branch runs.
- The fake returns ErrNotFound, using real lookup logic instead of a canned response.
A team is writing a test for a NotificationService that sends welcome emails. They want to verify that the service actually called the email sender with the correct recipient address — not just that the final state of some object is correct. Which type of test double is most appropriate for this purpose?
How It Works in Practice
Building a Stub in Go
// Interface your code depends on
type UserRepository interface {
GetByID(id string) (*User, error)
Save(user *User) error
}
// Stub for tests that need a user to exist
type StubUserRepository struct {
user *User
err error
}
func (r *StubUserRepository) GetByID(id string) (*User, error) {
return r.user, r.err
}
func (r *StubUserRepository) Save(user *User) error {
return r.err
}
// Test
func TestGetProfile_UserExists_ReturnsProfile(t *testing.T) {
repo := &StubUserRepository{
user: &User{ID: "abc", Name: "Alice"},
}
svc := NewUserService(repo)
profile, err := svc.GetProfile("abc")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if profile.Name != "Alice" {
t.Errorf("expected Alice, got %s", profile.Name)
}
}
func TestGetProfile_UserNotFound_ReturnsError(t *testing.T) {
repo := &StubUserRepository{err: ErrNotFound}
svc := NewUserService(repo)
_, err := svc.GetProfile("missing")
if err != ErrNotFound {
t.Errorf("expected ErrNotFound, got %v", err)
}
}Using testify/mock in Go
import "github.com/stretchr/testify/mock"
type MockEmailClient struct {
mock.Mock
}
func (m *MockEmailClient) Send(to, subject, body string) error {
args := m.Called(to, subject, body)
return args.Error(0)
}
func TestWelcomeEmail_NewUser_SendsEmail(t *testing.T) {
emailClient := new(MockEmailClient)
emailClient.On("Send", "alice@example.com", "Welcome!", mock.AnythingOfType("string")).
Return(nil)
svc := NewOnboardingService(emailClient)
err := svc.WelcomeUser("alice@example.com")
assert.NoError(t, err)
emailClient.AssertExpectations(t)
}Python: unittest.mock
from unittest.mock import MagicMock, patch
from billing import BillingService
def test_charge_customer_calls_gateway():
mock_gateway = MagicMock()
mock_gateway.charge.return_value = {"status": "success", "transaction_id": "txn_123"}
svc = BillingService(gateway=mock_gateway)
result = svc.charge_customer(customer_id="cust_1", amount=50.0)
mock_gateway.charge.assert_called_once_with(customer_id="cust_1", amount=50.0)
assert result["transaction_id"] == "txn_123"Using patch to replace a module-level dependency:
from unittest.mock import patch
def test_send_notification_calls_smtp():
with patch("notifications.smtp_client") as mock_smtp:
mock_smtp.send.return_value = True
send_notification("user@example.com", "Subject", "Body")
mock_smtp.send.assert_called_once()Controlling Time
Non-deterministic time is a common source of flaky tests. Inject clocks:
type Clock interface {
Now() time.Time
}
type RealClock struct{}
func (c *RealClock) Now() time.Time { return time.Now() }
type FixedClock struct{ t time.Time }
func (c *FixedClock) Now() time.Time { return c.t }
// In test:
fixedTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
clock := &FixedClock{t: fixedTime}
svc := NewTokenService(clock)
token := svc.GenerateToken("user-123")
// token's expiry is deterministic: fixedTime + expiry durationA mock checks the welcome email call
Configure the email mock, inject it into the onboarding service, then verify that the expected Send call occurred.
Expect a welcome email to Alice, and accept any string for the body.
Text version
- Expect a welcome email to Alice, and accept any string for the body.
- After injecting the mock, calling WelcomeUser makes the service send the expected email through it.
- The mock returns nil, so the service reports no error and the no-error assertion passes.
- AssertExpectations checks that the expected call occurred, because a successful return alone cannot prove this.
A test for a token generation service is occasionally failing because the token's expiry timestamp differs by a few milliseconds between runs. Which design change would make this test deterministic?
Common Mistakes
Using mocks for everything. Not every test needs a mock. Pure functions don't need any doubles. Reaching for a mock library by default couples your tests to implementation details. Start with stubs and fakes; use mocks only when you need to verify that a specific interaction occurred.
Stubbing the unit under test. You stub dependencies of the unit you're testing. Stubbing the unit itself means you're not testing it at all.
Mocking third-party packages directly. When you mock a library (like a Stripe SDK), you're testing against your assumed behavior of that library, not its actual behavior. Wrap third-party clients in an interface your code depends on; mock the interface, not the SDK.
Overly specific mock expectations. If a mock expects Send("alice@example.com", "Welcome!", "Dear Alice, your account is active") and the body changes slightly, the test fails even though the behavior is correct. Use argument matchers (AnythingOfType, mock.Anything) for parts you don't care about.
Not cleaning up doubles between tests. Shared mock state across tests causes order-dependent failures. Instantiate doubles fresh for each test; don't reuse them across test functions.
Your team has a mock set up for an email service that expects the exact call Send("alice@example.com", "Welcome!", "Dear Alice, your account is active"). A developer updates the email body to say "Dear Alice, welcome to the platform!" — a valid product change — but the test now fails. What is the best fix?
Tradeoffs
Doubles vs. real implementations. Fakes and stubs are faster and more controlled, but they can diverge from the real implementation. An in-memory store that doesn't enforce uniqueness constraints can let a test pass that would fail against a real DB with a unique index. Integration tests with real infrastructure catch this; unit tests with fakes don't.
Mocks vs. stubs. Mocks verify behavior (were the right calls made?). Stubs verify state (did the output have the right value?). Behavior verification is more brittle under refactoring. Prefer state verification (stubs + assertions on results) unless verifying interaction is the explicit goal.
Hand-written doubles vs. generated mocks. Hand-written stubs and fakes are verbose but simple to understand. Mock-generation libraries (Mockito, mockery for testify/mock) reduce boilerplate but add a dependency and can hide complexity. For small interfaces, hand-roll. For large interfaces, generate.
Test isolation vs. test realism. Fully isolated unit tests with doubles are fast and deterministic but test each piece in a vacuum. Real bugs often emerge from component interactions. The answer isn't to avoid doubles — it's to run both unit tests (with doubles) and integration tests (with real infrastructure), and understand what each layer tells you.
Use unit tests and integration tests together
Doubles give fast, controlled unit tests; integration tests with real infrastructure catch differences that a fake can miss.
Doubles make unit tests fast and controlled, while real systems introduce external variability.
Text version
- Doubles make unit tests fast and controlled, while real systems introduce external variability.
- A fake may allow duplicates that a database with a unique index would reject.
- Use isolated unit tests alongside integration tests, because component interactions can reveal bugs that doubles miss.
A team refactors an order service by splitting a single placeOrder() method into separate validateOrder() and persistOrder() methods internally, while keeping the same public interface and output. Tests that used mocks to verify that placeOrder() called specific internal methods now fail, even though the feature works correctly. Which testing approach would have been more resilient to this refactoring?
Quick Reference
| Type | Returns data? | Verifies calls? | Has real behavior? | Use when… |
|---|---|---|---|---|
| Dummy | No | No | No | Must satisfy a type but won't be used |
| Stub | Yes (fixed) | No | No | Need controlled inputs to test state |
| Fake | Yes (real logic) | No | Yes (simplified) | Need stateful behavior across calls |
| Spy | Yes (records) | Yes (manually) | No | Want to assert calls after the fact |
| Mock | Yes (configured) | Yes (automatic) | No | Want built-in call expectations |
Dependency injection pattern:
1. Define an interface for the dependency
2. Accept it in the constructor / function params
3. In tests, pass a double
4. In production, pass the real implementation
When to use which:
Pure function, no deps → no double needed
Read-only dependency → stub
Read/write stateful dependency → fake
Must verify "was X called?" → spy or mock
Third-party I/O → wrap in interface, then stub/mock the interfaceYou are testing a shopping cart service that applies discount rules and accumulates totals across multiple method calls. The dependency you need to replace tracks running totals and persists state between calls. Which test double is the best fit for this dependency?