Join our FREE personalized newsletter for news, trends, and insights that matter to everyone in America

Newsletter
New

Flutter Testing Guide: Unit, Widget, And Integration — When To Use Each

Card image cap

Flutter Testing Guide: Unit, Widget, and Integration — When to Use Each

Flutter gives you three test types. Knowing which to reach for—and when—is what makes testing feel useful rather than burdensome. Here's what I actually use in production.

The Three Layers

Unit Test:        verify logic in isolation (milliseconds)  
Widget Test:      verify UI behavior without a device (seconds)  
Integration Test: verify full user flows on an emulator (minutes)  

Build from the bottom up. Unit tests are your foundation. Integration tests are expensive — use them for critical paths only.

Unit Tests: Protect Business Logic

# pubspec.yaml  
dev_dependencies:  
  test: ^1.24.0  
// lib/utils/score_calculator.dart  
class ScoreCalculator {  
  static double calculate(int correct, int total) {  
    if (total == 0) return 0;  
    return correct / total * 100;  
  }  
}  
  
// test/utils/score_calculator_test.dart  
import 'package:test/test.dart';  
import 'package:my_app/utils/score_calculator.dart';  
  
void main() {  
  group('ScoreCalculator', () {  
    test('returns correct percentage', () {  
      expect(ScoreCalculator.calculate(8, 10), equals(80.0));  
    });  
  
    test('returns 0 when total is 0', () {  
      expect(ScoreCalculator.calculate(0, 0), equals(0.0));  
    });  
  });  
}  
flutter test test/utils/  # run unit tests only  

Widget Tests: Protect UI Behavior

// test/widgets/achievement_card_test.dart  
import 'package:flutter/material.dart';  
import 'package:flutter_test/flutter_test.dart';  
import 'package:my_app/widgets/achievement_card.dart';  
  
void main() {  
  testWidgets('shows title and description', (tester) async {  
    await tester.pumpWidget(  
      const MaterialApp(  
        home: AchievementCard(  
          title: 'First Test',  
          description: 'Wrote my first test',  
        ),  
      ),  
    );  
  
    expect(find.text('First Test'), findsOneWidget);  
    expect(find.text('Wrote my first test'), findsOneWidget);  
  });  
  
  testWidgets('calls onTap when tapped', (tester) async {  
    var tapped = false;  
    await tester.pumpWidget(  
      MaterialApp(  
        home: AchievementCard(  
          title: 'Test',  
          onTap: () => tapped = true,  
        ),  
      ),  
    );  
  
    await tester.tap(find.byType(AchievementCard));  
    expect(tapped, isTrue);  
  });  
}  

Riverpod: ProviderScope Overrides

// Override providers in widget tests  
testWidgets('shows loading indicator while fetching', (tester) async {  
  await tester.pumpWidget(  
    ProviderScope(  
      overrides: [  
        achievementsProvider.overrideWith(  
          (_) async {  
            await Future.delayed(const Duration(seconds: 1));  
            return [];  
          },  
        ),  
      ],  
      child: const MaterialApp(home: AchievementsPage()),  
    ),  
  );  
  
  await tester.pump();  // don't settle — stay in loading state  
  expect(find.byType(CircularProgressIndicator), findsOneWidget);  
  
  await tester.pumpAndSettle();  // settle → data shown  
  expect(find.byType(CircularProgressIndicator), findsNothing);  
});  

Mocking Supabase

// test/helpers/mock_supabase.dart  
import 'package:mocktail/mocktail.dart';  
import 'package:supabase_flutter/supabase_flutter.dart';  
  
class MockSupabaseClient extends Mock implements SupabaseClient {}  
class MockGoTrueClient extends Mock implements GoTrueClient {}  
  
// In your test setUp:  
setUp(() {  
  final mockClient = MockSupabaseClient();  
  final mockAuth = MockGoTrueClient();  
  when(() => mockClient.auth).thenReturn(mockAuth);  
  when(() => mockAuth.currentUser).thenReturn(null);  // unauthenticated  
});  

Integration Tests: E2E Flow Verification

// integration_test/login_flow_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('login to home flow', (tester) async {  
    app.main();  
    await tester.pumpAndSettle();  
  
    await tester.enterText(find.byKey(const Key('email')), 'test@example.com');  
    await tester.enterText(find.byKey(const Key('password')), 'password');  
    await tester.tap(find.byKey(const Key('login_button')));  
    await tester.pumpAndSettle();  
  
    expect(find.text('Home'), findsOneWidget);  
  });  
}  
flutter test integration_test/ --device-id emulator-5554  

CI Integration

# .github/workflows/ci.yml  
- name: Unit + Widget Tests  
  run: flutter test --coverage  
  
- name: Coverage check  
  run: |  
    lcov --summary coverage/lcov.info  
    # fail if coverage drops below 70%  

Where to Start

Step 1: Write Unit Tests for critical business logic  
Step 2: Write Widget Tests for UI you repeatedly test manually  
Step 3: Write one Integration Test for your most important user flow  

Start small, add tests as you go. Any tests are better than no tests.

Summary

  • Unit: highest ROI — fast, easy to write, catches logic regressions
  • Widget: catches UI regressions — use Riverpod overrides to isolate dependencies
  • Integration: catches flow regressions — expensive, limit to critical paths

Even as a solo developer, building a test habit is the foundation for shipping with confidence.