DEV Community

kanta13jp1
kanta13jp1

Posted on

Flutter Testing Complete Guide — Widget & Integration Tests for Quality Assurance

Flutter Testing Complete Guide — Widget & Integration Tests for Quality Assurance

Quality matters even in indie development. This guide covers the three-layer testing approach — Unit, Widget, and Integration — to keep your Flutter app reliable.

The Three Testing Layers

Type Speed Confidence Best For
Unit ⚡ Fastest Logic only Business logic, calculations
Widget 🚀 Fast UI + Logic Widget rendering, user interaction
Integration 🐢 Slow Near-real Screen flows, APIs, full journeys

Setup

dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter
  mockito: ^5.4.4
  build_runner: ^2.4.9
Enter fullscreen mode Exit fullscreen mode

Unit Tests

// test/unit/score_calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/services/score_calculator.dart';

void main() {
  group('ScoreCalculator', () {
    late ScoreCalculator calculator;
    setUp(() => calculator = ScoreCalculator());

    test('popularity bonus: rank 1 returns +0.05', () {
      expect(calculator.calculatePopularityBonus(rank: 1), closeTo(0.05, 0.001));
    });

    test('age bonus: 4 years old returns +0.02', () {
      expect(calculator.calculateAgeBonus(age: 4), closeTo(0.02, 0.001));
    });

    test('weight stability: within ±2kg returns +0.01', () {
      expect(
        calculator.calculateWeightBonus(previous: 460, current: 461),
        closeTo(0.01, 0.001),
      );
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Widget Tests

// test/widget/metric_card_test.dart
void main() {
  group('MetricCard', () {
    testWidgets('renders label and value correctly', (tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Scaffold(
            body: MetricCard(
              label: 'MRR',
              value: '\$980',
              trend: '+\$120',
              trendColor: Colors.green,
            ),
          ),
        ),
      );

      expect(find.text('MRR'), findsOneWidget);
      expect(find.text('\$980'), findsOneWidget);
      expect(find.text('+\$120'), findsOneWidget);
    });

    testWidgets('does not render trend when null', (tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Scaffold(body: MetricCard(label: 'LTV', value: '\$320')),
        ),
      );
      expect(find.text('\$320'), findsOneWidget);
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Testing Riverpod Providers

// test/widget/home_page_test.dart
void main() {
  testWidgets('displays MRR from provider', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          metricsProvider.overrideWith((ref) async => Metrics(
            mrr: 980,
            churnRate: 2.1,
            ltv: 320,
            activeSubscriptions: 100,
          )),
        ],
        child: const MaterialApp(home: HomePage()),
      ),
    );

    await tester.pumpAndSettle();
    expect(find.text('\$980'), findsOneWidget);
    expect(find.text('2.1%'), findsOneWidget);
  });
}
Enter fullscreen mode Exit fullscreen mode

Integration Tests

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

  group('Login → Home flow', () {
    testWidgets('email login succeeds', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      expect(find.byKey(const Key('login_page')), findsOneWidget);

      await tester.enterText(find.byKey(const Key('email_field')), 'test@example.com');
      await tester.enterText(find.byKey(const Key('password_field')), 'password123');
      await tester.tap(find.byKey(const Key('login_button')));
      await tester.pumpAndSettle(const Duration(seconds: 3));

      expect(find.byKey(const Key('home_page')), findsOneWidget);
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

GitHub Actions CI

# .github/workflows/flutter-test.yml
name: Flutter Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.27.0'
          channel: 'stable'
      - run: flutter pub get
      - run: flutter test --coverage
      - uses: codecov/codecov-action@v4
        with:
          file: coverage/lcov.info
Enter fullscreen mode Exit fullscreen mode

Coverage Check

flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
Enter fullscreen mode Exit fullscreen mode

Summary

The Unit → Widget → Integration testing pyramid gives you fast feedback at each layer. Riverpod's overrideWith makes mocking external dependencies clean. Automate with GitHub Actions so every PR gets tested without manual effort.


Building an AI Life Management app with Flutter × Supabase at 自分株式会社. Sharing indie dev insights every week.

Top comments (0)