Skip to main content

Writing Integration Tests

1. Setting Up Integration Tests

Integration tests in Flutter verify the interaction between different parts of your app, such as widgets and services. They run on a real or simulated device.

Setup Step Description Command/Code
Add dependencies Include flutter_driver in your pubspec.yaml
dev_dependencies:
  flutter_driver:
    sdk: flutter
  test: ^1.17.0
Create test directory Organize tests in test_driver folder mkdir test_driver
Write driver script Create a driver test file (e.g., integration_test.dart)
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Integration Test', () {
    final driver = FlutterDriver();
    // Test logic here
  });
}

Example: Basic Integration Test Setup

import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('App Integration Test', () {
    FlutterDriver driver;

    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    tearDownAll(() async {
      if (driver != null) {
        await driver.close();
      }
    });

    test('should launch app', () async {
      // Example assertion
      final app = await driver.getText(find.byType('MyApp'));
      expect(app, 'My App');
    });
  });
}
Note: Ensure your app is built with flutter build before running integration tests.
Warning: Integration tests require a real or emulator device to run.

2. Writing Integration Tests

Integration tests in Flutter verify the interaction between multiple components or modules of an app. These tests run on a real device or emulator and ensure that different parts of the app work together as expected.

Aspect Description Notes
Test file location test/integration_test/ Standard location for integration tests in Flutter projects
Test runner flutter test integration_test/ Command to run integration tests
Test type IntegrationTest Base class for integration tests

Example: Basic Integration Test Setup

// test/integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('App launches correctly', (WidgetTester tester) async {
    await tester.pumpWidget(const MyApp());
    expect(find.text('Hello, World!'), findsOneWidget);
  });
}

Example: Testing Navigation Flow

// test/integration_test/navigation_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('Navigates to detail screen', (WidgetTester tester) async {
    await tester.pumpWidget(const MyApp());
    await tester.tap(find.text('Go to Detail'));
    await tester.pumpAndSettle();
    expect(find.text('Detail Screen'), findsOneWidget);
  });
}
Note: Integration tests require the integration_test package to be added to your pubspec.yaml.
Warning: Ensure that your app is fully initialized before running integration tests, especially when using state management or async operations.

Key Points for Integration Tests

  • Use IntegrationTestWidgetsFlutterBinding.ensureInitialized() to initialize the binding.
  • Tests run on a real device or emulator, not in a simulated environment like unit tests.
  • Use testWidgets for widget-based integration tests.
  • Test navigation, state changes, and interactions between multiple widgets or screens.

3. Testing Full User Flows

Example: Testing a full user login flow

import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/main.dart';

void main() {
  testWidgets('User can login and navigate to dashboard', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());

    // Fill login form
    await tester.enterText(find.byKey(Key('email')), 'user@example.com');
    await tester.enterText(find.byKey(Key('password')), 'password123');
    await tester.tap(find.byKey(Key('loginButton'));
    await tester.pumpAndSettle();

    // Verify navigation to dashboard
    expect(find.text('Welcome, user@example.com'), findsOneWidget);
  });
}
Test Type Description Use Case
Widget Test Tests UI components and interactions Full user flow testing, component behavior
Integration Test Tests multiple components working together End-to-end user journeys, API integrations

Testing Full User Flows Steps

  1. Set up test environment with WidgetTester
  2. Build and pump the app widget tree
  3. Simulate user interactions (taps, text input)
  4. Assert expected UI state changes
  5. Verify navigation and data flow

Widget Test vs Integration Test

  • Widget Test: Unit testing of individual widgets
  • Integration Test: Tests multiple widgets and services together
Note: Use pumpAndSettle() to wait for animations and async operations to complete.

4. Using Native Automation

Example: Using Flutter Driver for Native Automation

import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Native Automation Test', () {
    FlutterDriver driver;

    setUp(() async {
      driver = await FlutterDriver.connect();
    });

    tearDown(() async {
      if (driver != null) {
        await driver.close();
      }
    });

    test('Verify login flow', () async {
      // Tap on login button
      await driver.tap(find.byValueKey('loginButton'));

      // Enter credentials
      await driver.enterText('user@example.com', find.byValueKey('emailField'));
      await driver.enterText('password123', find.byValueKey('passwordField'));

      // Tap submit
      await driver.tap(find.byValueKey('submitButton'));

      // Verify success screen
      expect(await driver.getText(find.byValueKey('successMessage')), 'Login Successful');
    });
  });
}
Feature Description Notes
FlutterDriver.connect() FlutterDriver instance for automation Connects to the running app under test
driver.tap() Simulates tap gesture Can tap on widgets by key, text, or type
driver.enterText() Enters text into input fields Supports value keys and text matching
find.byValueKey() Widget finder by key Most reliable way to identify widgets in tests
find.byText() Widget finder by text content Useful for verifying UI elements
Note: Native automation requires the app to be built in debug mode for Flutter Driver to connect.
Warning: Make sure to use setUp and tearDown to properly manage driver lifecycle.

Key Concepts

  • Widget Finding: Use find.byValueKey() for reliable widget identification.
  • Interaction Simulation: tap(), enterText() simulate real user actions.
  • Test Isolation: Each test should run in a clean state, with proper setup/teardown.

5. Running Integration Tests on Devices

Example: Running tests on a connected device

flutter drive --target='lib/main.dart' --driver='integration_test/app_test.dart'
Flag Description Usage
--target Path to the test file --target='lib/main.dart'
--driver Path to the driver file --driver='integration_test/app_test.dart'
--device-id ID of the device to run tests on --device-id='emulator-5554'
--verbose Enable verbose logging --verbose
Note: Ensure the device is connected and recognized by flutter devices.
Warning: Integration tests require the app to be built in debug mode.

Summary of Steps

  • Connect device via USB or start emulator
  • Run flutter devices to verify device is detected
  • Execute flutter drive with appropriate flags

6. Testing Platform Integrations

Feature Description Test Approach
Method Channel Used to communicate with platform-specific code (e.g., Android/iOS) Mock method channel responses using MethodChannel.setMockMethodCallHandler
Platform Channels Allows Flutter to call platform-specific APIs via channels Use TestWidgetsFlutterBinding.ensureInitialized() to set up platform channels in tests

Example: Mocking Platform Channel

import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Mock platform channel', (WidgetTester tester) async {
    const MethodChannel channel = MethodChannel('com.example.channel');
    channel.setMockMethodCallHandler((call) async {
      if (call.method == 'getData') {
        return {'data': 'mocked'};
      }
      return null;
    });

    // Your widget test logic here
  });
}
Note: Ensure that platform-specific code is properly mocked to avoid integration test failures.

Platform Integration Testing Tips

  • Use MethodChannel.setMockMethodCallHandler to simulate platform responses
  • Test with real and mocked platform channels to ensure robustness
  • Verify that your app handles platform errors gracefully

7. Using Patrol or Maestro for E2E

Patrol vs Maestro for Flutter E2E Testing

  • Patrol is a testing framework built specifically for Flutter, offering deep integration with the Flutter ecosystem and a more intuitive API for Flutter developers.
  • Maestro is a cross-platform testing tool that supports Flutter, Android, and iOS apps, allowing for tests to be written in YAML or JavaScript.
Feature Patrol Maestro
Test Language Dart YAML or JavaScript
Platform Support Flutter only Flutter, Android, iOS
Test Runner Flutter Driver Maestro CLI or Maestro Cloud
Test Recording Yes, via Patrol CLI Yes, via Maestro CLI
Test Debugging Yes, via Patrol CLI or IDE Yes, via Maestro CLI or Cloud
Test Reporting Yes, via Patrol CLI or Cloud Yes, via Maestro CLI or Cloud

Example: Patrol Test

import 'package:patrol/patrol.dart';

void main() {
  final tester = FlutterTester();

  tester.test('Login flow', (t) {
    t.pumpAndSettle();

    // Fill in login form
    t.enterText('username', 'user@example.com');
    t.enterText('password', 'password123');

    // Tap login button
    t.tap('loginButton');

    // Expect success screen
    t.expect(t.find.text('Welcome, user@example.com'));
  });
}

Example: Maestro Test

- name: Login Flow
  steps:
    - typeText:
        element: usernameInput
        text: user@example.com
    - typeText:
        element: passwordInput
        text: password123
    - tap:
        element: loginButton
    - assertText:
        element: welcomeMessage
        text: Welcome, user@example.com
Note: Both tools support recording and replaying tests, but Patrol offers more native integration with Flutter’s widget testing.
Warning: Maestro tests can be less stable in complex UIs due to its reliance on element IDs and accessibility labels.

8. Performance Testing

Aspect Description Tools/Methods
Frame Rate Monitoring Measure the app's frame rate to ensure it runs at 60 FPS for smooth UI. WidgetTester with pumpAndSettle() and performance metrics.
Memory Usage Monitor memory consumption during test runs to detect leaks or excessive usage. MemoryLeakTester or flutter test --coverage.
Build Performance Measure how long it takes to build and run tests. flutter test --verbose or flutter run --profile.

Example: Measuring Frame Rate

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';

void main() {
  testWidgets('Frame rate test', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(home: Text('Hello'))); 
    await tester.pumpAndSettle();
    
    // Measure frame rate using performance tools
    final frames = tester.takeExceptionFrames();
    expect(frames.length, greaterThan(0));
  });
}

Example: Memory Usage Test

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';

void main() {
  testWidgets('Memory usage test', (WidgetTester tester) async {
    final initialMemory = await getMemoryUsage();
    
    await tester.pumpWidget(MaterialApp(home: Text('Hello')));
    await tester.pumpAndSettle();
    
    final finalMemory = await getMemoryUsage();
    expect(finalMemory - initialMemory, lessThan(1024 * 1024)); // Less than 1MB increase
  });
}
Note: Use flutter test --machine to get detailed performance reports.

Key Performance Metrics

  • Frame rate (60 FPS target)
  • Memory consumption
  • Build and test execution time

9. Screenshot Testing

Screenshot testing in Flutter allows you to verify the visual appearance of your app by comparing rendered screenshots against expected baseline images.

Feature Description Notes
WidgetTester.pumpAndSettle() Waits for all animations and microtasks to complete before proceeding. Ensures stable UI state for screenshot capture.
WidgetTester.pumpWidget() Builds and renders a widget tree. Used to set up the UI before capturing screenshots.
WidgetTester.takeScreenshot() Captures a screenshot of the current widget tree. Can be used to compare against baseline images.

Example: Screenshot Test Setup

import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/main.dart';

void main() {
  testWidgets('Verify home screen appearance', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());
    await tester.pumpAndSettle();

    // Take a screenshot of the home screen
    await tester.takeScreenshot('home_screen');
  });
}
Note: Screenshots are captured in the test/screenshots directory by default. Ensure this directory exists or configure the path via flutter test --screenshots-dir=....
Warning: Screenshot tests can be brittle due to platform differences, font rendering, or UI changes. Use them for visual regression checks but not as a replacement for unit or widget tests.

10. Measuring Test Coverage

Example: Setting up code coverage with Flutter

flutter pub global activate coverage
flutter test --coverage
Feature Description Notes
coverage directory coverage/lcov.info Generated by flutter test --coverage
lcov format lcov.info Standard format for code coverage reports
coverage thresholds 80% Recommended minimum for production code
Note: Code coverage tools like lcov can be integrated with CI/CD pipelines to enforce coverage thresholds.

Code Coverage Tools for Flutter

  • lcov: Standard tool, integrates with flutter test --coverage
  • Coverage Dashboard: Web-based tool for visualizing coverage data
  • Codecov: Cloud service for code coverage reporting and integration with GitHub

Was this article helpful?