Skip to content

Testing

Flutter gives you four testing tools, and they trade speed for realism. Use many fast ones and few slow ones.

flowchart TB
  integ["Integration tests
(whole app, real device) — few, slow"] --> widget["Widget tests
(one widget tree, no device) — many, fast"]
  widget --> unit["Unit tests
(pure Dart logic) — most, fastest"]
The Flutter test pyramid
  • Unit tests verify pure Dart — a function, a class, a Cubit’s logic. No UI, no device, milliseconds each.
  • Widget tests pump a widget into a test harness and interact with it (tap, enter text, expect) — no real device, still fast. This is the Flutter-specific sweet spot.
  • Integration tests run the whole app on a real device or emulator, driving it end to end — slow, but the most realistic.
  • Golden tests capture a widget’s rendered pixels and compare against a saved reference image, catching visual regressions.
import 'package:test/test.dart';
test('cart total sums item prices', () {
final cart = Cart()..add(Item(price: 10))..add(Item(price: 5));
expect(cart.total, 15);
});

Plain Dart, plain expect. Most of your logic — pricing, validation, parsing, state transitions — should be reachable by tests this simple, which is a good reason to keep logic out of widgets.

Widget tests use WidgetTester to build a widget, find elements, act, and assert — all in memory.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
testWidgets('tapping + increments the counter', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp()); // build the widget tree
expect(find.text('0'), findsOneWidget); // initial state
await tester.tap(find.byIcon(Icons.add)); // interact
await tester.pump(); // rebuild after setState
expect(find.text('1'), findsOneWidget); // assert the new state
});

The key calls: pumpWidget builds the tree, find locates widgets, tap/enterText interact, and pump (or pumpAndSettle for animations) advances frames so rebuilds happen. No emulator needed — this runs in seconds.

testWidgets('button matches golden', (tester) async {
await tester.pumpWidget(const MyButton());
await expectLater(
find.byType(MyButton),
matchesGoldenFile('goldens/my_button.png'),
);
});

Golden (snapshot) tests are powerful for design systems, but brittle across platforms and font versions — pin them carefully and regenerate deliberately with --update-goldens.

What is a widget test?
In a widget test, what does `pump` do after an interaction?
Why keep business logic out of widgets?
What do golden tests catch?