skip to content

In Flutter tests of a forecast view model, when does a hand-written fake WeatherApi serve better than a mocktail mock, and how do you build it?

level: seniorimportance: should knowfreq 40%

answer

  1. state over interactions
  2. implements, not extends Mock
  3. compiler flags new interface members
  4. Completer for controllable latency
  5. extends Fake for partial doubles

basics

~20 s

Prefer a hand-written class that implements WeatherApi with in-memory data when many tests share the dependency or assert on resulting state; keep mocktail mocks for one-off stubs and for interactions that are themselves the behaviour.

solid answer

~40 s

A mock is configured per test with `when`, and interaction checks tie tests to how the view model calls the API. A fake — `class FakeWeatherApi implements WeatherApi` holding a map of forecasts and a flag to fail — is a small working implementation reused across suites; tests assert on the view model's state, so refactoring the calls does not break them. Because it uses `implements` with real bodies, the compiler reports every new interface member, unlike `noSuchMethod`-based `Mock` or `extends Fake`, which silently compile. Add a `Completer` to control latency so loading states can be tested. The Flutter docs' architecture case study uses fake repositories this way and a mocktail mock only for the router.

code

dart · 24 lines
dart
import 'dart:async';

import 'package:flutter_test/flutter_test.dart';

void main() {
  late FakeWeatherApi api;

  setUp(() => api = FakeWeatherApi()
    ..forecasts['Oslo'] = const Forecast(highC: 14, lowC: 6));

  test('shows loading, then the forecast', () async {
    api.gate = Completer<void>();
    final viewModel = ForecastViewModel(api);

    final pending = viewModel.load('Oslo');
    expect(viewModel.isLoading, isTrue);

    api.gate!.complete();
    await pending;

    expect(viewModel.isLoading, isFalse);
    expect(viewModel.forecast?.highC, 14);
  });
}

go deeper

for a junior

Know that a fake is a small real class implementing the interface, and a mocktail mock is configured with when and checked with verify.

for a middle

Explain how implements versus extends Mock or extends Fake changes what the compiler checks, and how a Completer makes loading states testable.

for a senior

Argue from refactor-resilience: state assertions against shared fakes survive implementation changes, while verification belongs only where the interaction is the behaviour.

for a principal

Set the convention: repository interfaces with shared fakes in a test package, mocks for one-offs, and ownership of fakes when interfaces change.

## The question behind the question Interviewers ask this to see whether a candidate writes tests that survive refactoring. In Flutter testing, the everyday choice for replacing a dependency such as a `WeatherApi` is between: - a **mocktail mock** — `class MockWeatherApi extends Mock implements WeatherApi {}`, configured per test with `when(...)` and checked with `verify(...)`; - a **hand-written fake** — a real, small class that `implements WeatherApi` with working in-memory behaviour. (The general vocabulary of test doubles is a separate topic; this is about what the Dart and Flutter tools give you.) ## What a fake looks like in Dart ```dart class FakeWeatherApi implements WeatherApi { final Map<String, Forecast> forecasts = {}; Object? failWith; Completer<void>? gate; int fetchCount = 0; @override Future<Forecast> fetchForecast(String city) async { fetchCount++; await gate?.future; if (failWith case final error?) throw error; return forecasts[city] ?? (throw StateError('no forecast for $city')); } } ``` It has **state** (the map), **controls** (`failWith`, `gate`) and optional **counters**. Nothing about it depends on a mocking library. ## When the fake wins 1. **Many tests share the dependency.** Ten tests each re-stubbing `fetchForecast` repeat setup; one fake with seeded data is read once and reused across the view-model, widget and integration suites. 2. **The assertion is about state.** "After `load('Oslo')`, `viewModel.forecast.highC` is 14" survives a refactor that batches, caches or retries requests. A `verify(...).called(1)` does not. 3. **The interface evolves.** A class that `implements WeatherApi` with real bodies fails to compile when a member is added, pointing straight at the stale double. A `Mock` subclass — and an `extends Fake` class — route missing members through `noSuchMethod`, so they compile and fail only at runtime, if at all. 4. **Timing matters.** Awaiting a `Completer` inside the fake lets a widget test pump the loading spinner, then complete the gate and pump the result. Doing the same with a mock means building the `Completer` in every test's `thenAnswer`. 5. **Behaviour has rules.** A fake repository that stores what you save and returns it later is simpler as code than as a chain of stubs. ## When the mock wins - **One-off stubs**, such as a single error case, where writing a class is heavier than one `when`. - **The interaction is the behaviour**: "a pull-to-refresh calls `fetchForecast` exactly once", "logout clears the token". Verification is the natural assertion there. - **Types you do not own and cannot construct**, like a router or a plugin facade, where a hand-written implementation would be large. ## `extends Fake` — the middle ground mocktail re-exports `Fake` from `package:test_api`. `class FakeForecastRequest extends Fake implements ForecastRequest {}` compiles with no bodies; any member you did not override throws `UnimplementedError`. It suits **partial** doubles and fallback values for `registerFallbackValue`. `package:test_api`'s own documentation notes that a full shared fake without `noSuchMethod` is usually preferable, and `extends Fake` is preferred over `extends Mock` mixed with hand-written overrides. | Double | Compile-time check of the interface | Reuse across suites | Best for | |---|---|---|---| | `implements WeatherApi` fake | yes | high | shared dependencies, state assertions | | `extends Fake implements` | no (`noSuchMethod`) | medium | partial doubles, fallback values | | `extends Mock implements` | no (`noSuchMethod`) | low | one-off stubs, interaction checks | ## What the Flutter docs do The Flutter documentation's architecture case study tests its view models against `FakeBookingRepository` and `FakeUserRepository` classes that implement the repository interfaces, and uses mocktail only to mock the router in widget tests. The layering matters: fakes are cheap when view models depend on **repository interfaces** rather than on HTTP clients directly. ## Pitfalls with fakes - A fake can **drift from reality** — returning data the real API never would. Keep it minimal and cover the real `WeatherApi` with its own tests against a `MockClient` or a staging contract. - Shared mutable fakes need a **fresh instance per test** in `setUp`, or state leaks between tests. - Counters on a fake are verification in disguise; use them sparingly, for the same reasons as `verify`. ## Where fakes live A fake that several suites share belongs in a test-only location — a `test/fakes/` folder, or a small `testing` library inside the package that owns the interface — so the owner of `WeatherApi` also owns `FakeWeatherApi` and updates it in the same change. A fake kept next to one test file tends to be copied, and copies drift. Keep fakes out of `lib/` code that ships, and keep them free of `flutter_test` imports when plain Dart tests should be able to use them too.

  • Why does a class that implements WeatherApi catch interface changes that a mocktail mock does not?
    A class using `implements` with real bodies must define every member, so adding a method to `WeatherApi` is a compile error in the fake. A `Mock` or `Fake` subclass forwards missing members to `noSuchMethod`, so it still compiles and only fails when a test happens to call the new member.
  • How do you stop a hand-written fake from drifting away from the real API's behaviour?
    Keep it minimal, mirror only documented behaviour, and give the real implementation its own tests against `MockClient` or a contract check. When a production bug reveals a real response the fake never produces, add that case to the fake.

saying these in an interview costs you the question

  • Mocks every dependency and verifies every call by default
  • Thinks extends Fake gives compile-time errors when the interface grows
  • Shares one mutable fake instance across tests without resetting it
  • Lets view models depend on the HTTP client, making fakes expensive
  • Assumes a fake never needs its own maintenance or tests