iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Testing

Flutter testing has three layers: unit (plain Dart), widget (no real device), and integration (real device). Use mocktail/Mockito for mocks, and run goldens for visual snapshots. The pyramid scales: many unit tests, fewer widget tests, very few integration tests.

Unit, widget, golden, and integration patterns

EXAMPLE
// pubspec.yaml (test deps)
// dev_dependencies:
//   flutter_test:
//     sdk: flutter
//   mocktail: ^1.0.0
//   integration_test:
//     sdk: flutter

// ===== 1) Pure Dart unit test =====
import 'package:flutter_test/flutter_test.dart';

int totalCents(List<Map<String, dynamic>> items) =>
    items.fold(0, (s, i) => s + (i['qty'] as int) * (i['priceCents'] as int));

void main() {
  group('totalCents', () {
    test('sums all items', () {
      expect(totalCents([
        {'qty': 2, 'priceCents': 250},
        {'qty': 1, 'priceCents': 500},
      ]), 1000);
    });

    test('returns 0 for empty list', () {
      expect(totalCents([]), 0);
    });
  });
}

// ===== 2) Widget test =====
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

class Counter extends StatefulWidget {
  const Counter({super.key});
  @override
  State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
  int n = 0;
  @override
  Widget build(BuildContext c) {
    return Material(
      child: Column(children: [
        Text('Count: $n'),
        ElevatedButton(onPressed: () => setState(() => n++), child: const Text('+')),
      ]),
    );
  }
}

void main_widget() {
  testWidgets('counter increments on tap', (tester) async {
    await tester.pumpWidget(const MaterialApp(home: Counter()));
    expect(find.text('Count: 0'), findsOneWidget);
    await tester.tap(find.text('+'));
    await tester.pump();
    expect(find.text('Count: 1'), findsOneWidget);
  });
}

// ===== 3) Mocking with mocktail =====
import 'package:mocktail/mocktail.dart';

abstract class Api { Future<String> getName(String id); }
class MockApi extends Mock implements Api {}

void main_mock() {
  test('shows fetched name', () async {
    final api = MockApi();
    when(() => api.getName('u1')).thenAnswer((_) async => 'Alice');
    expect(await api.getName('u1'), 'Alice');
    verify(() => api.getName('u1')).called(1);
  });
}

// ===== 4) Golden tests (visual snapshots) =====
void main_golden() {
  testWidgets('counter golden', (tester) async {
    await tester.pumpWidget(const MaterialApp(home: Counter()));
    await expectLater(find.byType(Counter), matchesGoldenFile('counter.png'));
  });
}
// Run: flutter test --update-goldens   (regenerate goldens after intentional UI change)
// CI runs without --update-goldens and fails on visual diff.

// ===== 5) Integration test (real device / emulator) =====
// integration_test/app_test.dart
// import 'package:flutter_test/flutter_test.dart';
// import 'package:integration_test/integration_test.dart';
// import 'package:my_app/main.dart' as app;
//
// void main() {
//   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
//
//   testWidgets('full happy path', (tester) async {
//     app.main();
//     await tester.pumpAndSettle();
//     await tester.tap(find.text('Sign in'));
//     await tester.pumpAndSettle();
//     // ... fill form, submit, assert next screen
//   });
// }
//
// Run on a device/emulator:
// flutter test integration_test

// ===== 6) Patterns to internalise =====
// - Unit tests first; widget tests for screens; integration tests for happy paths
// - Inject dependencies (api, clock) into widgets via constructor or InheritedWidget
// - Use 'pumpAndSettle' sparingly (it can hang on infinite animations); prefer 'pump' with explicit durations
// - Mock at the boundary; do not over-mock framework widgets

// ===== Pitfalls =====
// - testWidgets without await tester.pumpWidget(...) -> blank test
// - Animations + pumpAndSettle infinite loop -> use pump(duration)
// - Goldens that include OS-rendered fonts; pin a font via test setup
// - Async work not awaited inside tester.runAsync -> false-pass tests

Why it matters

Build the testing pyramid: many unit tests, fewer widget tests, very few integration tests. Unit tests catch logic bugs in milliseconds; widget tests prove the UI wires up; integration exists to prove "the whole thing still works" on a real device once per deploy. Invert the pyramid and you get slow flaky CI that nobody trusts.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
void main() {
    testWidgets('counter increments', (tester) async {
        await tester.pumpWidget(const MyApp());
        expect(find.text('0'), findsOneWidget);
        await tester.tap(find.byIcon(Icons.add));
        await tester.pump();
        expect(find.text('1'), findsOneWidget);
    });
}
Try it Yourself »

Discussion

Loading…