DEV Community

kanta13jp1
kanta13jp1

Posted on

Flutter Testing Strategy: Unit, Widget, Integration, and Golden Tests

Flutter Testing Strategy: Unit, Widget, Integration, and Golden Tests

"No time to write tests" vs "losing time to bugs" — which costs more? A practical guide to all four Flutter test types.

The Testing Pyramid

       /Golden\        ← few (UI snapshots)
      /Integration\    ← moderate (full screen flows)
     /  Widget    \    ← more (widget-level)
    /    Unit      \   ← most (business logic)
Enter fullscreen mode Exit fullscreen mode

Unit Tests: Guarantee Logic Correctness

import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/services/kpi_calculator.dart';

void main() {
  group('KpiCalculator', () {
    test('calculates DAU/MAU ratio correctly', () {
      final calc = KpiCalculator();
      expect(calc.dauMauRatio(dau: 100, mau: 500), equals(0.20));
    });

    test('returns 0 when MAU is 0', () {
      expect(KpiCalculator().dauMauRatio(dau: 0, mau: 0), equals(0.0));
    });

    test('MRR growth rate: 10000 → 12000 = 20%', () {
      expect(
        KpiCalculator().mrrGrowthRate(previous: 10000, current: 12000),
        equals(0.20),
      );
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Widget Tests: Test UI Components in Isolation

testWidgets('KpiCard shows title and value', (tester) async {
  await tester.pumpWidget(
    const MaterialApp(
      home: KpiCard(title: 'MAU', value: '1,234', trend: 0.15),
    ),
  );

  expect(find.text('MAU'),   findsOneWidget);
  expect(find.text('1,234'), findsOneWidget);
  expect(find.text('+15%'),  findsOneWidget);
});

testWidgets('negative trend renders in red', (tester) async {
  await tester.pumpWidget(
    const MaterialApp(
      home: KpiCard(title: 'Churn', value: '5%', trend: -0.03),
    ),
  );

  final text = tester.widget<Text>(find.text('-3%'));
  expect(text.style?.color, equals(Colors.red));
});
Enter fullscreen mode Exit fullscreen mode

Mocking Riverpod providers:

testWidgets('shows username from provider', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        userProfileProvider.overrideWith(
          (ref) => Future.value(Profile(id: '1', username: 'test_user')),
        ),
      ],
      child: const MaterialApp(home: ProfilePage()),
    ),
  );

  await tester.pump();
  expect(find.text('test_user'), findsOneWidget);
});
Enter fullscreen mode Exit fullscreen mode

Integration Tests: Test Full Screen Flows

// integration_test/login_flow_test.dart
void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('magic link login flow', (tester) async {
    await tester.pumpWidget(const MyApp());
    await tester.pumpAndSettle();

    await tester.enterText(
      find.byKey(const Key('email_field')),
      'test@example.com',
    );

    await tester.tap(find.byKey(const Key('send_magic_link_button')));
    await tester.pumpAndSettle();

    expect(find.text('Check your email'), findsOneWidget);
  });
}
Enter fullscreen mode Exit fullscreen mode

Run in CI:

- name: Integration tests (Chrome)
  run: |
    flutter test integration_test/ \
      -d chrome \
      --dart-define=SUPABASE_URL=${{ secrets.SUPABASE_URL }} \
      --dart-define=SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}
Enter fullscreen mode Exit fullscreen mode

Golden Tests: Protect Visual Appearance

testGoldens('KpiDashboard layout is stable', (tester) async {
  await tester.pumpWidgetBuilder(
    const KpiDashboard(),
    surfaceSize: const Size(375, 812),  // iPhone 12
  );

  await screenMatchesGolden(tester, 'kpi_dashboard');
  // First run: generates goldens/kpi_dashboard.png
  // Subsequent runs: pixel-diff against saved PNG → fails on changes
});
Enter fullscreen mode Exit fullscreen mode

Update goldens after intentional design changes:

flutter test --update-goldens test/golden/
Enter fullscreen mode Exit fullscreen mode

Decision Guide

Business logic (math, validation, transforms) → Unit
Widget display + interaction                  → Widget
Screen navigation, forms, API calls           → Integration
Prevent visual regressions                    → Golden
Enter fullscreen mode Exit fullscreen mode

Recommended split for indie devs:

Unit:        60% (logic breaks most often)
Widget:      25% (core components only)
Integration: 10% (happy path only)
Golden:       5% (landing page + key screens)
Enter fullscreen mode Exit fullscreen mode

Tests are an investment: writing time now vs. debugging time later. Start with Unit, add Widget for regressions, Integration for critical flows. Golden is optional but saves hours when you redesign.

Top comments (0)