Fakes vs mocks: practice with mocktail
The official recommendations put "make fakes for testing" at the highest priority, and the reason is concrete: a fake focuses on inputs and outputs rather than an object's inner workings — which forces you to write modular, well-defined interfaces.
The distinction, briefly:
- A fake is a simplified but working implementation of the contract. Calling
addgenuinely adds the item; the list is kept in memory. Written once, used across dozens of tests. - A mock is an object with no behaviour, programmed to answer instead. Each test writes a
when(() => …)setup and verifies calls withverify(…).
The criterion is one question: "am I verifying an outcome, or a call?"
- An outcome (the cart has three items, the total is 32, the status became
cancelled) → a fake. - A call (an analytics event was sent, the cache was invalidated,
logoutran once) → a mock.
The main mocking tool in Dart is mocktail: it needs no code generation, you declare MockCat extends Mock implements Cat, and calls are written in closures — when(() => cat.sound()).thenReturn('meow').
import 'package:mocktail/mocktail.dart';
import 'package:flutter_test/flutter_test.dart';
// 1) Elan: kod generasiyası YOXDUR, `build_runner` lazım deyil.
class MockAnalyticsService extends Mock implements AnalyticsService {}
class MockOrderRepository extends Mock implements OrderRepository {}
// 2) Öz tiplərin `any()` ilə işlədilirsə, fallback qeyd olunmalıdır.
class _FakeOrder extends Fake implements Order {}
void main() {
setUpAll(() {
// `any()` matcher-ı işlətməzdən əvvəl bir dəfə çağırılır.
registerFallbackValue(_FakeOrder());
});
late MockAnalyticsService analytics;
setUp(() {
analytics = MockAnalyticsService();
// Void metodlar üçün: thenAnswer((_) async {})
when(() => analytics.track(any(), properties: any(named: 'properties')))
.thenAnswer((_) async {});
});
test('sifariş verildikdə analitika hadisəsi göndərilir', () async {
final notifier = CheckoutNotifier(
repository: FakeOrderRepository([]), // NƏTİCƏ üçün fake
analytics: analytics, // ÇAĞIRIŞ üçün mock
);
await notifier.submit(testForm);
// Çağırışın yoxlanılması — burada mock haqlıdır,
// çünki "nəticə" yoxdur: hadisə göndərildi, vəssalam.
verify(() => analytics.track('order_placed',
properties: any(named: 'properties'))).called(1);
});
test('xəta halında analitika hadisəsi göndərilmir', () async {
final notifier = CheckoutNotifier(
repository: FakeOrderRepository([], placeError: const NetworkFailure()),
analytics: analytics,
);
await notifier.submit(testForm);
verifyNever(() => analytics.track('order_placed',
properties: any(named: 'properties')));
});
test('stub-ın qaytardığı dəyər', () {
final repo = MockOrderRepository();
// Sinxron dəyər üçün thenReturn, Future üçün thenAnswer.
when(() => repo.fetchMine())
.thenAnswer((_) async => Result.ok(const <Order>[]));
// Arqumentə görə fərqli cavab:
when(() => repo.cancel('o-1'))
.thenAnswer((_) async => Result.ok(cancelledOrder));
when(() => repo.cancel('o-2'))
.thenAnswer((_) async => const Result.error(NotFoundFailure()));
// Xəta atmaq:
when(() => repo.fetchById(any())).thenThrow(const NetworkFailure());
});
}
// mocktail-in mockito-dan əsas fərqləri:
// • kod generasiyası yoxdur (@GenerateMocks, build_runner lazım deyil)
// • çağırışlar closure-a bükülür: when(() => ...), verify(() => ...)
// • tip-spesifik matcher-lar (anyString, anyInt) yerinə vahid `any()`,
// `any(named: '...')`, `any(that: ...)`mocktail's core API: declaration, stubbing, verification and `registerFallbackValue`.
| Criterion | Fake | Mock (mocktail) |
|---|---|---|
| What is verified | The outcome (state, returned value) | That a call happened |
| Cost to write | Once, 20-40 lines | One line to declare, but a `when` setup in every test |
| Reuse | High — across all tests | Low — the setup is test-specific |
| Sensitivity to refactoring | Low — works as long as the contract holds | High — breaks when a name or parameter changes |
| Real behaviour | Yes (keeps state in memory) | No (it only answers) |
| Best fit | Repository, service and use case contracts | Analytics, logging, notifications — side effects |
Three properties of a good fake.
1. Working state. It keeps a list or Map in memory, and add, update, delete genuinely take effect. That lets tests verify behaviour like "adding twice increases the quantity".
2. Error injection. Open the error path through a constructor parameter: FakeOrderRepository(const [], fetchError: NetworkFailure()). Then you do not need a separate fake class per error case.
3. Counters. fetchCount, cancelCount — for verifying caching and coalescing behaviour. That gives you something like a mock's verify without losing the fake's behaviour.
When to add latency. A test fake should have no latency — tests should be fast. Latency is needed in only two situations: (1) in a …Local implementation (previous stage — to see the loading state with your own eyes), (2) in one specific test that checks a race condition (via a placeDelay parameter).
Fixtures (test data). Writing Order(id: 'o-1', title: …, status: …, total: …) in every test is duplication and fragile: adding a new required field to the model breaks 40 test files. The fix is builder functions in test/fixtures/: anOrder(status: OrderStatus.pending). When a field is added, only the builder changes.
A mocktail trap. If you use the any() matcher with your own types, registerFallbackValue must be called — otherwise the test throws at runtime. Doing it once in setUpAll is enough.
Practice. Two steps:
1. Create test/fixtures/order_fixtures.dart with a builder: Order anOrder({String? id, OrderStatus? status, double? total}), giving every field a default.
2. Write a complete fake for one repository contract: working state, error injection (a constructor parameter) and two counters. Then use it in at least three tests: a notifier test, a widget test and an error-path test.
Done means: adding a field to the model changes only the builder file (no tests break), and checking an error case needs no new class.
📚 Sources and documentation
- The mocktail packageofficialpub.dev
`when(() => …)`, `verify(() => …)`, `any()`, `registerFallbackValue` — the source of the API used here.
- Architecture recommendations: fakesofficialdocs.flutter.dev
The "make fakes for testing" recommendation and its justification.
- Case study: testing each layerofficialdocs.flutter.dev
How fakes like `FakeBookingRepository` and `FakeApiClient` are written in a real project.
- Flutter: unit testsofficialdocs.flutter.dev
The basics of `setUp`, `setUpAll`, `group` and test structure.