Flutter has three kinds of tests, each answering a different question. Unit tests check your Dart logic. Widget tests check that a widget renders and responds to taps. Integration tests check the whole app running on a real device.

Use unit and widget tests for most things. Skip integration tests until the app is complex enough to earn them.

Unit

Test pure Dart logic with no widgets. These are fast, so run them on every save.

dart
// test/counter_test.dart
void main() {
test('increment adds 1', () {
  final counter = Counter();
  counter.increment();
  expect(counter.value, 1);
});
}

Run: flutter test

Group related tests with group('description', () { ... }) to keep the output organized.

Widget

Widget tests render a widget in a test environment and let you interact with it. Slower than unit tests, still quick enough to run often.

dart
testWidgets('shows counter and increments', (tester) async {
await tester.pumpWidget(MaterialApp(home: Counter()));

expect(find.text('0'), findsOneWidget);

await tester.tap(find.byIcon(Icons.add));
await tester.pump();

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

pumpWidget renders the widget. pump triggers a rebuild after an interaction. find locates widgets by text, icon, or type. expect asserts what should be on screen.

Integration

Integration tests run the full app on a real device or emulator. They're the slowest, but they catch bugs the other two can't (animations, real network calls, platform-specific bugs).

dart
// integration_test/app_test.dart
void main() {
testWidgets('sign-up flow', (tester) async {
  await tester.pumpWidget(MyApp());
  await tester.enterText(find.byType(TextField).first, 'test@test.com');
  await tester.tap(find.text('Sign Up'));
  await tester.pumpAndSettle();
  expect(find.text('Welcome'), findsOneWidget);
});
}

Run: flutter test integration_test

pumpAndSettle waits for all animations and frames to finish before continuing. Use it after any action that kicks off navigation or a network call.

Mocktail

Most tests need to stand in for something else (a database, an API, a billing SDK). mocktail is the package I use to create fake versions of those dependencies.

Install: flutter pub add --dev mocktail

dart
class MockApi extends Mock implements Api {}

test('fetches users', () async {
final api = MockApi();
when(() => api.getUsers()).thenAnswer((_) async => [User('Mitch')]);

final users = await api.getUsers();
expect(users.length, 1);
});

when(...).thenAnswer(...) sets up what the mock should return. Your code then hits the mock, not the real API.

What I actually test

  • Logic that can break silently: auth, billing, payments, payments, anything money-adjacent.
  • Core flows: sign-up, checkout, the main journey a user takes through your app.

What I skip: trivial UI, single-line getters, framework code. Tests earn their place by catching real regressions, not by padding coverage numbers.