Unit, widget and integration tests

Flutter · Advanced · 16 min

Widget tests run in milliseconds without a device and catch UI regressions before users do.

Flutter supports three kinds of tests:

  • Unit tests — plain Dart logic (view models, repositories, validators) with package:test APIs: test, expect, group.
  • Widget tests — render widgets in a headless test environment with testWidgets and a `WidgetTester`. Fast, no device needed.
  • Integration tests — integration_test package; run the full app on a device or emulator, for end-to-end flows.

A widget test:

testWidgets('increments', (tester) async {
  await tester.pumpWidget(const MaterialApp(home: CounterPage()));
  await tester.tap(find.byIcon(Icons.add));
  await tester.pump();
  expect(find.text('1'), findsOneWidget);
});

Key APIs:

  • pumpWidget builds the widget; wrap it in MaterialApp (and providers) as needed.
  • pump() triggers a frame after an interaction; pump(duration) advances time; pumpAndSettle() pumps until animations finish (it times out with infinite animations).
  • Finders: find.text, find.byType, find.byKey(ValueKey('save')), find.byIcon, find.widgetWithText.
  • Matchers: findsOneWidget, findsNothing, findsNWidgets(n).
  • Interactions: tap, enterText, drag, longPress.

Inject fakes for repositories through constructors or providers instead of real HTTP. Real network calls in widget tests return 400 by default. Golden tests (matchesGoldenFile) compare rendered pixels to a stored image. Run with flutter test, and flutter test --coverage for coverage.

Example

// test/like_button_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class LikeButton extends StatefulWidget {
  const LikeButton({super.key});
  @override
  State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
  int likes = 0;
  @override
  Widget build(BuildContext context) => TextButton(
      key: const ValueKey('like'), onPressed: () => setState(() => likes++), child: Text('$likes likes'));
}
void main() {
  testWidgets('tapping like increments the count', (tester) async {
    await tester.pumpWidget(const MaterialApp(home: Scaffold(body: LikeButton())));
    expect(find.text('0 likes'), findsOneWidget);
    await tester.tap(find.byKey(const ValueKey('like')));
    await tester.pump();                       // rebuild after setState

    expect(find.text('1 likes'), findsOneWidget);
    expect(find.text('0 likes'), findsNothing);
  });
}

Remember

  • Unit tests for logic, widget tests for UI, integration tests for full flows
  • pumpWidget, interact, then pump() before asserting
  • Find by key, text, type or icon; inject fakes instead of real HTTP

Watch out for

  • Forgetting await tester.pump() after a tap, so the expectation sees the old UI.

Check yourself

  1. After `await tester.tap(...)` triggers setState, why call `await tester.pump()` before checking the text?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.flutter.dev/testing/overview