Testing
Unit tests, widget tests, integration tests, mocking
- 01
What are the three types of tests in Flutter and when to use each?
MediumFlutter has three test layers that trade speed against confidence: unit, widget, and integration.
// ── Unit Test ───────────────────────────────────── import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockUserRepo extends Mock implements UserRepository {} … - 02
How do you write golden tests in Flutter?
MediumGolden tests (snapshot tests) compare a widget's rendered output pixel-by-pixel against a saved "golden" reference image.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; void main() { // ── Basic golden test ──────────────────────────── testWidgets('ProductCard golden', (tester) async { … - 03
How do you test widgets that depend on InheritedWidgets (Theme, MediaQuery, etc.)?
MediumA widget reading Theme, MediaQuery or Localizations throws in a test unless a scope above it provides them.
import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; // ── One helper, used by every test in the suite ── Future<void> pumpApp(WidgetTester tester, Widget child, {ThemeData? theme}) { return tester.pumpWidget(MaterialApp( … - 04
How do you choose between mocktail and mockito for mocking dependencies?
MediumBoth let you fake classes for tests; the choice mostly comes down to ergonomics and null safety.
// pubspec.dev: mocktail: ^1.0.0 import 'package:mocktail/mocktail.dart'; import 'package:flutter_test/flutter_test.dart'; class MockUserApi extends Mock implements UserApi {} … - 05
What's the difference between pump, pumpAndSettle, pumpFrames, and runAsync in widget tests?
MediumWidget tests run on a fake clock, so a five-second delay in your code costs the test nothing — you advance time yourself.
testWidgets('animation reaches end', (tester) async { await tester.pumpWidget(MaterialApp(home: const _Animated())); // ✅ Step through animation frame-by-frame for (var t = Duration.zero; t < const Duration(milliseconds: 600); t += const Duration(milliseconds: 16)) { await tester.pump(const Duration(milliseconds: 16)); … - 06
How do you write integration tests with integration_test or patrol?
HardIntegration tests drive the whole app on a real device or emulator, with only the network faked at the boundary.
import 'package:integration_test/integration_test.dart'; import 'package:my_app/main.dart' as app; void main() { // integration_test/checkout_flow_test.dart IntegrationTestWidgetsFlutterBinding.ensureInitialized(); … - 07
How do you mock HTTP/Dio in tests without real network calls?
MediumReal HTTP is forbidden in CI — it is flaky, slow, and depends on a staging environment being up.
// ── 1) Fake repository — the cleanest ── class FakeUsersRepo implements UsersRepository { @override Future<User> byId(int id) async => User(id: id, name: 'Ada'); @override Future<List<User>> all({int page = 1}) async => List.generate(3, (i) => User(id: i, name: 'u$i')); } … - 08
How do you measure code coverage and gate it in CI?
MediumCoverage tells you which lines of source ran during tests.
# .github/workflows/test.yml name: Test + Coverage on: [pull_request] jobs: test: … - 09
How do you test a screen that uses Riverpod or BLoC?
MediumBoth libraries let you swap the dependency layer for fakes and then assert either the emitted state or the rendered UI.
// ── Riverpod — unit test with ProviderContainer ── import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockApi extends Mock implements ApiClient {} … - 10
In a widget test, how do you prove that tapping a button navigated to the right screen?
MediumPrefer asserting that the destination is on screen after the tap, and fall back to a mock NavigatorObserver when the destination is too heavy to build.
testWidgets('cart button opens checkout', (tester) async { await tester.pumpWidget(MaterialApp( home: const CartPage(), routes: {'/checkout': (_) => const CheckoutPage()}, )); … - 11
A widget under test calls a plugin and the test throws MissingPluginException. What are your options?
HardThere is no platform behind a Flutter test, so every channel call has to be answered by a fake — either at the plugin's Dart interface or at the binary messenger.
void main() { TestWidgetsFlutterBinding.ensureInitialized(); const channel = MethodChannel('com.example/battery'); final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; … - 12
How do you test a debounce, a retry with backoff, or a poll without making the suite slow?
MediumNever sleep in a test — run the code against a fake clock and move time forward yourself.
test('debounce emits once after the quiet period', () { fakeAsync((async) { final seen = <String>[]; final search = Debouncer(const Duration(milliseconds: 300)); search.run(() => seen.add('a')); … - 13
A widget test fails with "Found 2 widgets with text Save" — how do you narrow the finder without reaching for .first?
MediumScope the finder to the subtree or the widget you mean, because
tester.tapandfindsOneWidgetboth demand exactly one match and an index just follows whatever the layout does next.testWidgets('confirming the dialog saves the note', (tester) async { await tester.pumpWidget(const MaterialApp(home: NoteEditor())); await tester.tap(find.byTooltip('Edit')); await tester.pumpAndSettle(); // ❌ throws: the AppBar title and the dialog button both say 'Save' … - 14
A widget test taps the 40th row of a ListView and fails with "Nothing found", but the screen works fine in the app — what is happening?
MediumListView.builderonly builds what fits the 800x600 test surface plus its cache extent, so row 40 is not in the element tree at all and no finder can see it — the test has to scroll, exactly like a user.testWidgets('tapping a far-down row opens it', (tester) async { await tester.pumpWidget(const MaterialApp(home: OrdersPage())); // ❌ nothing found: ListView.builder never built row 40 // await tester.tap(find.text('Order 40')); … - 15
Golden tests have made every local flutter test run slow — how do you keep them out of the fast loop but still run them in CI?
MediumTag them and declare the tags in
dart_test.yaml, then select tags on the command line —flutter testruns on package:test's runner and reads that file from the package root.# dart_test.yaml — package root, read by `flutter test` tags: golden: # renders and diffs images: slower than a unit test timeout: 2x # 2x the 30s default slow: … - 16
You renamed a private method and thirty tests went red without a single behaviour change — what were those tests doing wrong?
MediumThey asserted on how the code works instead of what it does, so a refactor — the exact thing tests are supposed to make safe — broke them.
// ❌ Brittle: asserts the interaction and the tree shape testWidgets('loads the cart', (tester) async { final repo = MockCartRepo(); when(() => repo.fetchPage(1)).thenAnswer((_) async => [item]); await tester.pumpWidget(MaterialApp(home: CartPage(repo: repo))); … - 17
How do you assert that a Cubit emits loading and then data, without sprinkling delays and hoping the timing works out?
HardAttach the expectation with
expectLaterand a stream matcher before you trigger the work, then await it — the matcher consumes events as they arrive, so nothing depends on how long the work takes.test('loads the profile', () async { final cubit = ProfileCubit(FakeProfileRepo()); // ✅ subscribe first, then act — no delays anywhere final expectation = expectLater( cubit.stream, … - 18
The suite is green on your machine and fails one random test per CI run — how do you actually track that down?
HardSuspect shared state before timing: files run in parallel isolates, but every test inside a file shares the same globals, so the first candidate is something a previous test left behind.
// ❌ Leaks into every later test in the file void main() { setUpAll(() { getIt.registerSingleton<CartRepo>(FakeCartRepo()); // built once, mutated by all }); … - 19
Your widget throws inside build when the API returns null, and the test dies with that exception — how do you assert on it instead?
HardClaim it with
tester.takeException(): the test binding records every exception the framework caught and rethrows anything still unclaimed when the test ends.testWidgets('a malformed payload surfaces as a FormatException', (tester) async { await tester.pumpWidget( MaterialApp(home: PriceTag(raw: 'not-a-number')), ); // The framework already caught it; claim it or the test fails at the end … - 20
How do you catch an unlabelled icon button or a 40dp tap target in the test suite instead of in a screen-reader review?
Hardflutter_testships accessibility guideline matchers, but the semantics tree is built lazily, so a test has to turn it on withtester.ensureSemantics()before any of them mean anything.testWidgets('checkout screen meets the accessibility guidelines', (tester) async { final handle = tester.ensureSemantics(); // without this the tree is empty addTearDown(handle.dispose); await tester.pumpWidget( MaterialApp(theme: appTheme, home: const CheckoutPage()), …