Files
Musify/musify_flutter/.agents/skills/flutter-testing/references/integration-testing.md
T

14 KiB

Integration Testing Guide

Overview

Integration tests test complete apps or large parts of apps on real devices or emulators. They verify that all widgets and services work together as expected and can measure performance.

When to Write Integration Tests

  • Testing complete user flows across multiple screens
  • Verifying navigation between pages
  • Testing state persistence
  • Validating real device behavior (sensors, camera, etc.)
  • Performance profiling and benchmarking
  • Testing platform-specific features

Setup

Add Dependency

Add to pubspec.yaml:

dev_dependencies:
  integration_test:
    sdk: flutter
  flutter_test:
    sdk: flutter

Create Test Driver

Create test_driver/integration_test.dart when using flutter drive, browser integration tests, screenshots, or response data from performance traces:

import 'package:integration_test/integration_test_driver.dart';

Future<void> main() => integrationDriver();

Test Structure

Basic 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();
  
  testWidgets('app launches and displays home', (tester) async {
    await tester.pumpWidget(const MyApp());
    
    expect(find.text('Home Screen'), findsOneWidget);
  });
}

Running Integration Tests

# Run on all devices
flutter test integration_test/

# Run on specific device
flutter test -d <device-id> integration_test/my_test.dart

# Run specific test file
flutter test integration_test/my_test.dart

# Run a browser/driver integration test
flutter drive \
  --driver=test_driver/integration_test.dart \
  --target=integration_test/my_test.dart \
  -d chrome

Testing User Flows

Multi-Screen Navigation

testWidgets('complete user flow from login to dashboard', (tester) async {
  await tester.pumpWidget(const MyApp());
  
  // Login screen
  expect(find.text('Login'), findsOneWidget);
  
  // Enter credentials
  await tester.enterText(find.byKey(const Key('username')), 'user@example.com');
  await tester.enterText(find.byKey(const Key('password')), 'password123');
  
  // Tap login
  await tester.tap(find.byKey(const Key('login-button')));
  await tester.pumpAndSettle();
  
  // Dashboard screen
  expect(find.text('Dashboard'), findsOneWidget);
  expect(find.text('Welcome, user@example.com'), findsOneWidget);
});

Tab Navigation

testWidgets('navigate between tabs', (tester) async {
  await tester.pumpWidget(const MyApp());
  
  // Initial tab
  expect(find.text('Home'), findsOneWidget);
  
  // Tap second tab
  await tester.tap(find.text('Profile'));
  await tester.pumpAndSettle();
  
  expect(find.text('Home'), findsNothing);
  expect(find.text('Profile'), findsOneWidget);
  
  // Navigate back
  await tester.tap(find.text('Home'));
  await tester.pumpAndSettle();
  
  expect(find.text('Home'), findsOneWidget);
});

Testing Forms

Complete Form Submission

testWidgets('submit registration form', (tester) async {
  await tester.pumpWidget(const MyApp());
  
  // Navigate to registration
  await tester.tap(find.text('Sign Up'));
  await tester.pumpAndSettle();
  
  // Fill form
  await tester.enterText(find.byKey(const Key('name')), 'John Doe');
  await tester.enterText(find.byKey(const Key('email')), 'john@example.com');
  await tester.enterText(find.byKey(const Key('password')), 'password123');
  await tester.enterText(find.byKey(const Key('confirm-password')), 'password123');
  
  // Submit
  await tester.tap(find.byKey(const Key('submit')));
  await tester.pumpAndSettle();
  
  // Verify success
  expect(find.text('Registration Successful'), findsOneWidget);
});

Form Validation

testWidgets('form shows validation errors', (tester) async {
  await tester.pumpWidget(const MyApp());
  
  await tester.tap(find.text('Sign Up'));
  await tester.pumpAndSettle();
  
  // Submit empty form
  await tester.tap(find.byKey(const Key('submit')));
  await tester.pump();
  
  // Verify errors
  expect(find.text('Name is required'), findsOneWidget);
  expect(find.text('Email is required'), findsOneWidget);
  expect(find.text('Password is required'), findsOneWidget);
  
  // Fill with invalid email
  await tester.enterText(find.byKey(const Key('email')), 'invalid-email');
  await tester.tap(find.byKey(const Key('submit')));
  await tester.pump();
  
  expect(find.text('Invalid email format'), findsOneWidget);
});

Testing Lists and Data

Loading and Displaying Data

testWidgets('load and display items from API', (tester) async {
  await tester.pumpWidget(const MyApp());
  
  // Wait for data load
  await tester.pumpAndSettle();
  
  // Verify items loaded
  expect(find.byType(ListTile), findsWidgets);
  expect(find.text('Item 1'), findsOneWidget);
  expect(find.text('Item 2'), findsOneWidget);
});

Pull-to-Refresh

testWidgets('pull to refresh updates list', (tester) async {
  await tester.pumpWidget(const MyApp());
  await tester.pumpAndSettle();
  
  // Pull down
  await tester.drag(
    find.byType(RefreshIndicator),
    const Offset(0, 300),
  );
  await tester.pumpAndSettle();
  
  // Verify refreshed data
  expect(find.text('Updated Item 1'), findsOneWidget);
});

Infinite Scrolling

testWidgets('load more items on scroll', (tester) async {
  await tester.pumpWidget(const MyApp());
  await tester.pumpAndSettle();
  
  // Initial items
  expect(find.text('Item 20'), findsNothing);
  
  // Scroll to bottom to trigger load more
  await tester.fling(
    find.byType(ListView),
    const Offset(0, -2000),
    10000,
  );
  await tester.pumpAndSettle();
  
  // New items loaded
  expect(find.text('Item 20'), findsOneWidget);
});

Testing State Persistence

Persisted Settings

testWidgets('user preference persists across app rebuild', (tester) async {
  final preferences = FakePreferencesStore();

  await tester.pumpWidget(MyApp(preferences: preferences));
  
  // Change setting
  await tester.tap(find.text('Dark Mode'));
  await tester.pumpAndSettle();
  
  // Rebuild the app with the same injected storage.
  await tester.pumpWidget(const SizedBox.shrink());
  await tester.pumpWidget(MyApp(preferences: preferences));
  await tester.pumpAndSettle();
  
  // Verify setting persisted
  expect(find.text('Light Mode'), findsNothing);
  expect(find.text('Dark Mode'), findsOneWidget);
});

Authentication State

testWidgets('user stays logged in', (tester) async {
  final authStore = FakeAuthStore();

  await tester.pumpWidget(MyApp(authStore: authStore));
  
  // Login
  await tester.enterText(find.byKey(const Key('email')), 'user@example.com');
  await tester.enterText(find.byKey(const Key('password')), 'password');
  await tester.tap(find.byKey(const Key('login')));
  await tester.pumpAndSettle();
  
  expect(find.text('Dashboard'), findsOneWidget);
  
  // Rebuild the app with the same injected auth store.
  await tester.pumpWidget(const SizedBox.shrink());
  await tester.pumpWidget(MyApp(authStore: authStore));
  await tester.pumpAndSettle();
  
  // Still logged in
  expect(find.text('Dashboard'), findsOneWidget);
  expect(find.text('Login'), findsNothing);
});

Performance Testing

Measuring Frame Time

testWidgets('measure scrolling performance', (tester) async {
  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
      as IntegrationTestWidgetsFlutterBinding;

  await tester.pumpWidget(const MyApp());
  await tester.pumpAndSettle();
  
  await binding.watchPerformance(() async {
    await tester.fling(
      find.byType(ListView),
      const Offset(0, -1000),
      5000,
    );
    await tester.pumpAndSettle();
  }, reportKey: 'scrolling_performance');
});

Tracking Build Count

testWidgets('widget doesn't rebuild unnecessarily', (tester) async {
  int buildCount = 0;
  
  await tester.pumpWidget(
    MaterialApp(
      home: Builder(
        builder: (context) {
          buildCount++;
          return const MyWidget();
        },
      ),
    ),
  );
  
  expect(buildCount, 1);
  
  // Perform unrelated action
  await tester.tap(find.byType(ElevatedButton));
  await tester.pumpAndSettle();
  
  // Should not rebuild
  expect(buildCount, 1);
});

Memory Tracking on Native Targets

import 'dart:io' show ProcessInfo;

testWidgets('monitor memory usage during scrolling', (tester) async {
  await tester.pumpWidget(const MyApp());
  
  final initialMemory = ProcessInfo.currentRss;
  
  await tester.fling(
    find.byType(ListView),
    const Offset(0, -5000),
    10000,
  );
  await tester.pumpAndSettle();
  
  final finalMemory = ProcessInfo.currentRss;
  final memoryIncrease = finalMemory - initialMemory;
  
  // Memory increase should be reasonable (< 10MB)
  expect(memoryIncrease, lessThan(10 * 1024 * 1024));
});

Testing Platform-Specific Features

Permissions

testWidgets('request and handle camera permission', (tester) async {
  final permissions = FakePermissionGateway(
    camera: PermissionStatus.granted,
  );

  await tester.pumpWidget(MyApp(permissions: permissions));
  
  // Request camera access
  await tester.tap(find.text('Take Photo'));
  await tester.pumpAndSettle();
  
  // Verify camera screen appears
  expect(find.byType(CameraPreview), findsOneWidget);
});
testWidgets('handle deep link to product page', (tester) async {
  await tester.pumpWidget(const MyApp(initialRoute: '/product/123'));
  await tester.pumpAndSettle();
  
  expect(find.text('Product #123'), findsOneWidget);
});

In-App Purchases

testWidgets('complete purchase flow', (tester) async {
  await tester.pumpWidget(const MyApp());
  
  await tester.tap(find.text('Premium'));
  await tester.pumpAndSettle();
  
  await tester.tap(find.text('Subscribe'));
  await tester.pumpAndSettle();
  
  // Verify purchase completed
  expect(find.text('Premium Active'), findsOneWidget);
});

Testing Network

Offline Behavior

testWidgets('handle offline state gracefully', (tester) async {
  final connectivity = FakeConnectivity(isOnline: false);
  final api = FakeApiClient(networkError: const SocketException('offline'));
  
  await tester.pumpWidget(MyApp(connectivity: connectivity, apiClient: api));
  await tester.pumpAndSettle();
  
  // Verify offline message
  expect(find.text('No internet connection'), findsOneWidget);
  
  // Simulate back online
  connectivity.isOnline = true;
  api.networkError = null;
  await tester.tap(find.byKey(const Key('retry-button')));
  await tester.pumpAndSettle();
  
  // Reload data
  expect(find.text('Data loaded'), findsOneWidget);
});

Error Handling

testWidgets('handle network errors', (tester) async {
  await tester.pumpWidget(const MyApp());
  
  // Trigger API call
  await tester.tap(find.text('Load Data'));
  await tester.pumpAndSettle();
  
  // Verify error message displayed
  expect(find.text('Failed to load data'), findsOneWidget);
  expect(find.byKey(const Key('retry-button')), findsOneWidget);
});

Testing Animations

Smooth Animations

testWidgets('animation runs smoothly', (tester) async {
  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
      as IntegrationTestWidgetsFlutterBinding;

  await tester.pumpWidget(const MyApp());
  
  await binding.traceAction(() async {
    await tester.tap(find.byKey(const Key('animate')));
    await tester.pumpAndSettle();
  }, reportKey: 'animation_timeline');
});

Best Practices

  1. Test critical user paths - Focus on important flows
  2. Keep tests independent - Each test should be standalone
  3. Use descriptive names - Explain what the test does
  4. Wait for async operations - Use pumpAndSettle()
  5. Test real behavior - Verify actual device behavior, not mocks
  6. Measure performance - Track frame times, memory, and build counts
  7. Test edge cases - Offline, errors, empty states
  8. Keep tests fast - Avoid unnecessary delays

Debugging Integration Tests

Take Screenshots

testWidgets('debug test with screenshot', (tester) async {
  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
      as IntegrationTestWidgetsFlutterBinding;

  await tester.pumpWidget(const MyApp());
  
  // On Android, call convertFlutterSurfaceToImage and pump before screenshots.
  await binding.convertFlutterSurfaceToImage();
  await tester.pump();
  await binding.takeScreenshot('test-screenshot');
  
  // Perform actions
  await tester.tap(find.text('Button'));
  await tester.pumpAndSettle();
  
  // Take another screenshot
  await binding.takeScreenshot('after-tap');
});

Print Widget Tree

testWidgets('print widget tree for debugging', (tester) async {
  await tester.pumpWidget(const MyApp());
  
  // Print widget tree
  debugDumpApp();
});

CI/CD Integration

GitHub Actions

name: Integration Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: macos-latest
    
    steps:
      - uses: actions/checkout@v2
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.19.0'
      
      - name: Get dependencies
        run: flutter pub get
      
      - name: Run integration tests
        run: flutter test integration_test/

Test Reports

# Generate test report
flutter test integration_test/ --reporter expanded

# Generate JSON report
flutter test integration_test/ --reporter json > test-results.json