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 |
|
| 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) |
|
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
testWidgetsfor 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
- Set up test environment with
WidgetTester - Build and pump the app widget tree
- Simulate user interactions (taps, text input)
- Assert expected UI state changes
- 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 devicesto verify device is detected - Execute
flutter drivewith 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.setMockMethodCallHandlerto 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.
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.