DEV Community

kanta13jp1
kanta13jp1

Posted on

Flutter Testing Advanced — Integration Tests, Mocking, and CI Automation

Introduction

Once you've mastered unit testing in Flutter, the next step is combining E2E tests, mocking strategies, and CI automation. This article walks through practical patterns using integration_test, mockito/mocktail, and GitHub Actions — everything you need to build a production-grade test suite.


1. E2E Testing with the integration_test Package

flutter_driver is deprecated. The current official recommendation is the integration_test package.

# pubspec.yaml
dev_dependencies:
  integration_test:
    sdk: flutter
  flutter_test:
    sdk: flutter
Enter fullscreen mode Exit fullscreen mode

Place test files in the integration_test/ directory:

// 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('E2E login flow test', (tester) async {
    app.main();
    await tester.pumpAndSettle();

    // Enter credentials
    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.text('Dashboard'), findsOneWidget);
  });
}
Enter fullscreen mode Exit fullscreen mode

Run it with:

flutter test integration_test/app_test.dart -d chrome
Enter fullscreen mode Exit fullscreen mode

2. mockito vs mocktail — Dependency Injection and Mock Creation

mockito (code generation approach)

// test/auth_service_test.dart
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'auth_service_test.mocks.dart';

@GenerateMocks([SupabaseClient])
void main() {
  late MockSupabaseClient mockClient;
  late AuthService authService;

  setUp(() {
    mockClient = MockSupabaseClient();
    authService = AuthService(client: mockClient);
  });

  test('returns user on successful login', () async {
    when(mockClient.auth.signInWithPassword(
      email: anyNamed('email'),
      password: anyNamed('password'),
    )).thenAnswer((_) async => AuthResponse(/* ... */));

    final result = await authService.login('test@example.com', 'pass');
    expect(result.user, isNotNull);
  });
}
Enter fullscreen mode Exit fullscreen mode

Generate mocks with build_runner:

dart run build_runner build --delete-conflicting-outputs
Enter fullscreen mode Exit fullscreen mode

mocktail (no code generation required)

import 'package:mocktail/mocktail.dart';

class MockSupabaseClient extends Mock implements SupabaseClient {}

void main() {
  test('simple mock with mocktail', () async {
    final mock = MockSupabaseClient();
    when(() => mock.from('users').select()).thenReturn(MockPostgrestFilterBuilder());
    // ...
  });
}
Enter fullscreen mode Exit fullscreen mode

mocktail shines for quick tests where the overhead of code generation isn't worth it.


3. Supabase Mock Client Pattern

Design your repositories behind an interface to make Supabase easy to mock:

abstract class ISupabaseRepository {
  Future<List<Map<String, dynamic>>> fetchTasks();
}

class SupabaseRepository implements ISupabaseRepository {
  final SupabaseClient _client;
  SupabaseRepository(this._client);

  @override
  Future<List<Map<String, dynamic>>> fetchTasks() async {
    final response = await _client.from('tasks').select();
    return List<Map<String, dynamic>>.from(response);
  }
}

// Fake implementation for tests — no network calls
class FakeSupabaseRepository implements ISupabaseRepository {
  @override
  Future<List<Map<String, dynamic>>> fetchTasks() async {
    return [{'id': 1, 'title': 'Test Task', 'done': false}];
  }
}
Enter fullscreen mode Exit fullscreen mode

Inject FakeSupabaseRepository in your widget tests to keep them fast and deterministic.


4. Running flutter test --machine in GitHub Actions

# .github/workflows/flutter-test.yml
name: Flutter Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.22.0'
          channel: 'stable'

      - name: Install dependencies
        run: flutter pub get

      - name: Run unit & widget tests
        run: flutter test --machine | tee test-results.json

      - name: Run integration tests (Chrome)
        run: |
          flutter test integration_test/ -d chrome \
            --machine | tee integration-results.json
Enter fullscreen mode Exit fullscreen mode

The --machine flag emits JSON output — ideal for aggregating results or feeding into test reporting tools.


5. Automatically Updating Golden Tests in CI

Golden tests capture UI snapshots. When the UI intentionally changes, update them using --update-goldens:

      - name: Update goldens on main branch
        if: github.ref == 'refs/heads/main'
        run: flutter test --update-goldens test/golden/

      - name: Commit updated goldens
        if: github.ref == 'refs/heads/main'
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: 'chore: update golden test snapshots'
          file_pattern: 'test/golden/*.png'
Enter fullscreen mode Exit fullscreen mode

On PR branches, goldens are not updated — the CI simply reports diffs. After merging to main, they are updated automatically. This prevents accidental snapshot drift while still catching unintended visual regressions.


Summary

Technique Best for
integration_test E2E / widget integration tests
mockito Type-safe mocks with code generation
mocktail Quick mocks without code generation
--machine + GHA Aggregating CI test results
Golden test auto-update Managing intentional UI snapshot changes

Think of your test suite as a pyramid: Unit tests at the base (fast and numerous), widget tests in the middle, and a thin layer of integration tests at the top. This structure minimizes maintenance cost while maximizing confidence in your Flutter app.

Top comments (0)