Lesson 36 of 77 advanced

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

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. 1. class FakeUserRepository implements UserRepository — implements the REAL interface so it can substitute the real implementation in any test
  2. 2. Map _store — in-memory storage; behaves like a real repository but without I/O
  3. 3. void seed(User user) — a test-only helper not in the interface; adds setup data without going through the normal save flow
  4. 4. testGoldens('WorkspaceCard...') — golden_toolkit wrapper that handles font loading and device pixel ratio
  5. 5. await loadAppFonts() — critical: without this, text renders in the fallback font and your golden test catches font mismatches
  6. 6. screenMatchesGolden(tester, 'workspace_card_active') — renders the widget and saves/compares against workspace_card_active.png
  7. 7. BadRepository creates its own Dio — impossible to inject a mock, so it cannot be unit tested without a real network
  8. 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

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery