flutter dart
npx skills add dhruvanbhalara/skills --skill flutter-add-integration-test

Contents

Project Setup

  1. Add required development dependencies to pubspec.yaml:

    flutter pub add 'dev:integration_test:{"sdk":"flutter"}'
    flutter pub add 'dev:flutter_test:{"sdk":"flutter"}'
    
  2. Create directory structure:

    project_root/
    ├── integration_test/
    │   └── app_test.dart          # Test cases
    └── test_driver/
        └── integration_test.dart  # Host driver script
    
  3. Create the host driver script at test_driver/integration_test.dart:

    import 'package:integration_test/integration_test_driver.dart';
    
    Future<void> main() => integrationDriver();
    
  4. Add ValueKeys to critical widgets in production code for reliable targeting:

    FloatingActionButton(
      key: const ValueKey('increment_fab'),
      onPressed: _increment,
      child: const Icon(Icons.add),
    )
    

Test Authoring

  • Initialize the binding at the top of main() — this replaces the default test binding.
  • Load the full application with tester.pumpWidget(const MyApp()).
  • Use tester.pumpAndSettle() after every interaction to wait for animations and async operations.
  • Assert widget visibility using expect(find.byKey(ValueKey('foo')), findsOneWidget).
  • Scroll to off-screen widgets using tester.scrollUntilVisible(finder, 500.0).

Test File Structure

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('End-to-end test', () {
    testWidgets('complete user flow', (tester) async {
      // Load full app
      await tester.pumpWidget(const MyApp());

      // Interact with widgets
      await tester.tap(find.byKey(const ValueKey('login_button')));
      await tester.pumpAndSettle();

      // Assert navigation happened
      expect(find.byType(HomePage), findsOneWidget);
    });
  });
}

Execution Targets

Choose the execution method based on target platform:

Local Device (Android/iOS)

flutter test integration_test/

For iOS, when running under the default Swift Package Manager (SPM) integration (Flutter 3.44+), ensure you run package resolution first:

xcodebuild -resolvePackageDependencies -workspace ios/Runner.xcworkspace -scheme Runner

Chrome (Web)

# Terminal 1: Start ChromeDriver
chromedriver --port=4444

# Terminal 2: Run tests
flutter drive \
  --driver=test_driver/integration_test.dart \
  --target=integration_test/app_test.dart \
  -d chrome

Headless Web

flutter drive \
  --driver=test_driver/integration_test.dart \
  --target=integration_test/app_test.dart \
  -d web-server

Firebase Test Lab (Android)

# 1. Build debug APK
flutter build apk --debug

# 2. Build instrumentation test APK
pushd android && ./gradlew app:assembleAndroidTest && popd

# 3. Upload both APKs to Firebase Test Lab via console or gcloud:
gcloud firebase test android run \
  --type instrumentation \
  --app build/app/outputs/flutter-apk/app-debug.apk \
  --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk

Performance Profiling

Wrap test actions in binding.traceAction() to capture performance timelines:

void main() {
  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('scrolling performance', (tester) async {
    await tester.pumpWidget(const MyApp());

    await binding.traceAction(() async {
      final listFinder = find.byType(Scrollable);
      await tester.fling(listFinder, const Offset(0, -500), 10000);
      await tester.pumpAndSettle();
    }, reportKey: 'scrolling_timeline');
  });
}

Performance Profiling Driver

Use this driver to capture and write timeline data:

import 'package:flutter_driver/flutter_driver.dart' as driver;
import 'package:integration_test/integration_test_driver.dart';

Future<void> main() {
  return integrationDriver(
    responseDataCallback: (data) async {
      if (data != null) {
        final timeline = driver.Timeline.fromJson(
          data['scrolling_timeline'] as Map<String, dynamic>,
        );
        final summary = driver.TimelineSummary.summarize(timeline);
        await summary.writeTimelineToFile(
          'scrolling_timeline',
          pretty: true,
          includeSummary: true,
        );
      }
    },
  );
}

CI/CD Integration

GitHub Actions Workflow

- name: Run integration tests
  uses: reactivecircus/android-emulator-runner@v2
  with:
    api-level: 33
    script: flutter test integration_test/ --flavor dev
  • Use reactivecircus/android-emulator-runner for Android emulator.
  • Collect test result artifacts with actions/upload-artifact.
  • For web, run chromedriver as a service and test with -d chrome.

Common Pitfalls

Error Cause Fix
PumpAndSettleTimedOutException Infinite animation (e.g., CircularProgressIndicator) Use pump() instead, or dismiss the loading state
Widget not found Lazy-loaded in SliverList or ListView Call scrollUntilVisible() before interacting
Test hangs Network call in production code Mock HTTP client or use --dart-define to bypass
No host driver specified Missing test_driver/integration_test.dart Create the host driver file

Workflow: Adding an Integration Test

Task Progress

  • [ ] Step 1: Add integration_test and flutter_test to dev_dependencies.
  • [ ] Step 2: Assign ValueKeys to target widgets in production code.
  • [ ] Step 3: Create integration_test/app_test.dart with binding initialization.
  • [ ] Step 4: Create test_driver/integration_test.dart with integrationDriver().
  • [ ] Step 5: Write test cases — load app, interact, assert.
  • [ ] Step 6: Choose execution target:
    • Local device: flutter test integration_test/
    • Chrome: flutter drive ... -d chrome
    • Firebase Test Lab: build + upload APKs
  • [ ] Step 7: Feedback Loop:
    • If PumpAndSettleTimedOutException → check for infinite animations.
    • If widget not found → add scrollUntilVisible.
    • Re-run until all tests pass.

Examples

Standard Integration Test

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('Counter app', () {
    testWidgets('tap FAB, verify counter increments', (tester) async {
      await tester.pumpWidget(const MyApp());

      // Verify initial state
      expect(find.text('0'), findsOneWidget);

      // Tap the increment button
      final fab = find.byKey(const ValueKey('increment_fab'));
      await tester.tap(fab);
      await tester.pumpAndSettle();

      // Verify counter incremented
      expect(find.text('1'), findsOneWidget);
    });
  });
}

Multi-Screen Navigation Flow

testWidgets('login and navigate to home', (tester) async {
  await tester.pumpWidget(const MyApp());

  // Enter credentials
  await tester.enterText(find.byKey(const ValueKey('email_field')), 'user@test.com');
  await tester.enterText(find.byKey(const ValueKey('password_field')), 'password123');

  // Submit login
  await tester.tap(find.byKey(const ValueKey('login_button')));
  await tester.pumpAndSettle();

  // Verify navigation to home
  expect(find.byType(HomePage), findsOneWidget);
  expect(find.byType(LoginPage), findsNothing);
});