Testability Patterns: Mocks, Fakes, Golden Tests & Coverage
Writing code that is easy to test — the design and tooling decisions that separate senior engineers
Open interactive version (quiz + challenge)Real-world analogy
A Mock is a strict actor who only does what the script says and shouts if you deviate. A Fake is an understudy who knows the whole role and performs naturally. A Stub is a cardboard cutout that just stands there holding a prop. Knowing which one to put on stage is the art of testability.
What is it?
Testability patterns are design and tooling choices that make code easy to test in isolation. They include choosing the right test double (mock, fake, stub), using golden file tests for visual regression, understanding coverage metrics, and architecting code with dependency injection and abstractions.
Real-world relevance
On a multi-workspace collaboration app, golden tests for the WorkspaceCard component caught that a dependency update to the design system package changed the accent colour from #2196F3 to #1976D2 — invisible in code review but immediately obvious in a golden test diff.
Key points
- Mock vs Fake vs Stub — Mock: a generated object that records interactions and fails if unexpected methods are called. Fake: a lightweight real implementation (e.g., in-memory database). Stub: hardcoded return values, no verification. Each has the right use case.
- When to use a Mock — Use mocks when you need to VERIFY interactions — confirm a method was called with specific arguments a specific number of times. Use for collaborators where calling them has side effects (sending emails, writing to DB).
- When to use a Fake — Use fakes when the real implementation is too heavy (database, file system) but you want realistic behaviour. A FakeUserRepository with an in-memory Map is faster and more realistic than a mock for testing complex workflows.
- When to use a Stub — Use stubs for simple read-only dependencies where you only care about the return value, not whether the method was called. They are the simplest option — prefer them when verification is not needed.
- Golden file testing — Golden tests render a widget and save a PNG screenshot. On subsequent runs, the widget is re-rendered and compared pixel-by-pixel to the golden file. Any UI change fails the test until you approve and update the golden file.
- Running golden tests — Run flutter test --update-goldens to generate or update golden files. Run flutter test normally to compare. Golden files should be committed to git and reviewed in PRs — they are the visual specification.
- What good coverage means — Coverage measures which lines of code were executed by tests. 80%+ on business logic (use cases, repositories, BLoCs) is a practical target. 100% is often counterproductive — trivial getters and generated code inflate the number.
- Making code testable by design — Testability is not added after — it is designed in. Inject dependencies through constructors, depend on abstractions (abstract classes/interfaces), keep functions pure (no hidden state), avoid singletons without interfaces.
- Dependency injection and testability — A class that creates its own Dio instance cannot be unit tested. A class that receives Dio through its constructor can be tested with a MockDio. Constructor injection is the #1 testability pattern in Flutter.
- Golden tests for design systems — On a SaaS app with a shared component library, golden tests for every reusable widget (buttons, cards, badges) prevent accidental visual regressions when updating shared dependencies or theming.
Code example
// === FAKE: in-memory implementation for complex workflow tests ===
class FakeUserRepository implements UserRepository {
final Map<String, User> _store = {};
@override
Future<User> getUserById(String id) async {
final user = _store[id];
if (user == null) throw UserNotFoundException(id);
return user;
}
@override
Future<void> saveUser(User user) async {
_store[user.id] = user;
}
// Test helper — not part of the interface
void seed(User user) => _store[user.id] = user;
}
// === GOLDEN TEST: visual regression for a UI component ===
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
void main() {
testGoldens('WorkspaceCard renders correctly for active workspace', (tester) async {
await loadAppFonts(); // golden_toolkit helper to load real fonts
await tester.pumpWidgetBuilder(
WorkspaceCard(
workspace: Workspace(
id: 'ws-1',
name: 'Acme Corp',
memberCount: 12,
isActive: true,
),
),
surfaceSize: const Size(375, 120),
);
await screenMatchesGolden(tester, 'workspace_card_active');
});
}
// === COVERAGE: run in terminal ===
// flutter test --coverage
// genhtml coverage/lcov.info -o coverage/html
// open coverage/html/index.html
// === TESTABLE DESIGN: constructor injection ===
// UNTESTABLE — creates its own dependency
class BadRepository {
final _dio = Dio(); // Cannot be mocked
Future<User> getUser(String id) => _dio.get('/users/$id').then(...);
}
// TESTABLE — dependency injected
class GoodRepository {
final Dio _dio; // Can be replaced with MockDio in tests
GoodRepository(this._dio);
Future<User> getUser(String id) => _dio.get('/users/$id').then(...);
}Line-by-line walkthrough
- 1. class FakeUserRepository implements UserRepository — implements the REAL interface so it can substitute the real implementation in any test
- 2. Map _store — in-memory storage; behaves like a real repository but without I/O
- 3. void seed(User user) — a test-only helper not in the interface; adds setup data without going through the normal save flow
- 4. testGoldens('WorkspaceCard...') — golden_toolkit wrapper that handles font loading and device pixel ratio
- 5. await loadAppFonts() — critical: without this, text renders in the fallback font and your golden test catches font mismatches
- 6. screenMatchesGolden(tester, 'workspace_card_active') — renders the widget and saves/compares against workspace_card_active.png
- 7. BadRepository creates its own Dio — impossible to inject a mock, so it cannot be unit tested without a real network
- 8. GoodRepository receives Dio via constructor — swap in MockDio in tests; swap in real Dio in production via dependency injection
Spot the bug
class FakeAuthRepository implements AuthRepository {
bool _isLoggedIn = false;
@override
Future<bool> login(String email, String password) async {
if (email == 'test@test.com') {
_isLoggedIn = true;
return true;
}
return false;
}
@override
Future<void> logout() async {
_isLoggedIn = false;
}
}
// Test
void main() {
test('login returns true for valid credentials', () async {
final repo = FakeAuthRepository();
final result = await repo.login('test@test.com', 'anypassword');
expect(result, isTrue);
});
test('isLoggedIn is true after login', () async {
final repo = FakeAuthRepository();
expect(repo._isLoggedIn, isTrue);
});
}Need a hint?
Look at the second test carefully — it does not call login() first.
Show answer
Bug: The second test checks repo._isLoggedIn without first calling repo.login(). _isLoggedIn starts as false, so the test will always fail. Additionally, accessing _isLoggedIn (a private field) from outside the class is an anti-pattern — the test is testing internal state rather than behaviour. Fix: call await repo.login('test@test.com', 'pass') before the assertion, and expose isLoggedIn as a public getter if the interface requires it.
Explain like I'm 5
Imagine testing a pizza delivery app. A Mock delivery driver checks their script every step and reports 'wrong!' if you deviate. A Fake delivery driver is a junior colleague who actually drives around but uses a small test city. A Stub is a Post-It note that just says 'pizza delivered' whenever you check. Each has the right job — the art is choosing which one.
Fun fact
The term 'golden test' comes from the concept of a 'golden master' — in manufacturing, the first approved perfect sample that all subsequent samples are compared against. If they match the golden master, they pass quality control.
Hands-on challenge
Identify one class in a project that is difficult to test because it creates its own dependencies internally. Refactor it to use constructor injection and write one unit test using a Mock and one using a Fake for the same scenario. Compare which is easier to maintain.
More resources
- golden_toolkit package (pub.dev)
- Flutter Test Doubles — mocktail (pub.dev)
- flutter test --coverage (Flutter Docs)
- Mocks Aren't Stubs — Martin Fowler (Martin Fowler)