Comprehensive Flutter testing guidance covering unit tests, widget tests, and integration tests. Use when working with Flutter applications to write unit tests for functions/methods/classes, create widget tests to verify UI components, develop integration tests for end-to-end testing, mock dependencies and plugin interactions, debug common testing errors, test Flutter plugins with native code, and run tests in different build modes (debug, profile, release)
This skill provides comprehensive guidance for testing Flutter applications across all test types. Flutter testing falls into three categories:
A well-tested Flutter app has many unit and widget tests for code coverage, plus enough integration tests to cover important use cases.
| Tradeoff | Unit | Widget | Integration |
|---|---|---|---|
| Confidence | Low | Higher | Highest |
| Maintenance cost | Low | Higher | Highest |
| Dependencies | Few | More | Most |
| Execution speed |
| Quick |
| Quick |
| Slow |
Flutter supports three build modes with different implications for testing:
Unit tests test a single function, method, or class. Mock external dependencies and avoid disk I/O or UI rendering.
import 'package:test/test.dart';
import 'package:my_app/counter.dart';
void main() {
test('Counter value should be incremented', () {
final counter = Counter();
counter.increment();
expect(counter.value, 1);
});
}
Run with: flutter test
Widget tests test single widgets to verify UI appearance and interaction.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('MyWidget has a title and message', (tester) async {
await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));
final titleFinder = find.text('T');
final messageFinder = find.text('M');
expect(titleFinder, findsOneWidget);
expect(messageFinder, findsOneWidget);
});
}
Integration tests test complete apps on real devices or emulators.
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('tap button, verify counter', (tester) async {
await tester.pumpWidget(const MyApp());
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byKey(const ValueKey('increment')));
await tester.pumpAndSettle();
expect(find.text('1'), findsOneWidget);
});
}
Run with: flutter test integration_test/
What are you testing?
Does it depend on plugins/native code?
Need to mock dependencies?
Encountering errors?
Unit tests verify the correctness of a unit of logic under various conditions.
package:test/test.dartFor mocking dependencies, plugin interactions, and complex scenarios, see Unit Testing Reference.
Widget tests verify widget UI appearance and behavior in a test environment.
// By text
final titleFinder = find.text('Title');
// By widget type
final buttonFinder = find.byType(ElevatedButton);
// By key
final fabFinder = find.byKey(const ValueKey('increment'));
// By widget instance
final myWidgetFinder = find.byWidget(myWidgetInstance);
// Tap
await tester.tap(buttonFinder);
// Drag
await tester.drag(listFinder, const Offset(0, -300));
// Enter text
await tester.enterText(fieldFinder, 'Hello World');
// Scroll
await tester.fling(listFinder, const Offset(0, -500), 10000);
await tester.pumpAndSettle();
testWidgets('widget in landscape mode', (tester) async {
// Set to landscape
await tester.binding.setSurfaceSize(const Size(800, 400));
await tester.pumpWidget(const MyApp());
// Verify landscape behavior
expect(find.byType(MyWidget), findsOneWidget);
// Reset to portrait
addTearDown(tester.binding.setSurfaceSize(null));
});
For scrolling, complex interactions, and performance testing, see Widget Testing Reference.
Integration tests test complete apps or large parts on real devices or emulators.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('end-to-end test', () {
testWidgets('complete user flow', (tester) async {
await tester.pumpWidget(const MyApp());
// Step 1: Navigate to screen
await tester.tap(find.text('Login'));
await tester.pumpAndSettle();
// Step 2: Enter credentials
await tester.enterText(find.byKey(const Key('username')), 'user');
await tester.enterText(find.byKey(const Key('password')), 'pass');
// Step 3: Submit
await tester.tap(find.text('Submit'));
await tester.pumpAndSettle();
// Verify result
expect(find.text('Welcome'), findsOneWidget);
});
});
}
testWidgets('scrolling performance', (tester) async {
await tester.pumpWidget(const MyApp());
final listFinder = find.byType(ListView);
// Measure performance
final timeline = await tester.trace(() async {
await tester.fling(listFinder, const Offset(0, -500), 10000);
await tester.pumpAndSettle();
});
// Analyze timeline data
expect(timeline.frames.length, greaterThan(10));
});
For performance profiling, CI integration, and complex scenarios, see Integration Testing Reference.
When testing code that uses plugins, special handling is required to avoid crashes.
If your Flutter app uses plugins, you need to mock the platform channel calls in unit/widget tests.
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
// Mock platform channel
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('your.plugin.channel'),
(MethodCall methodCall) async {
if (methodCall.method == 'getPlatformVersion') {
return 'Android 12';
}
return null;
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('your.plugin.channel'),
null,
);
});
}
For comprehensive guidance on testing Flutter plugins (including native code), see Plugin Testing Reference.
Yellow and black stripes indicate overflow. Usually caused by unconstrained children in Row/Column.
Solution: Wrap the overflowing widget in Expanded or Flexible.
// Problem
Row(
children: [
Icon(Icons.message),
Column(children: [Text('Very long text...')]), // Overflow!
],
)
// Solution
Row(
children: [
Icon(Icons.message),
Expanded(child: Column(children: [Text('Very long text...')])),
],
)
Occurs when ListView (or other scrollable) is inside Column without height constraints.
Solution: Wrap in Expanded or use shrinkWrap: true.
// Problem
Column(
children: [
Text('Header'),
ListView(children: [...]), // Error!
],
)
// Solution
Column(
children: [
Text('Header'),
Expanded(child: ListView(children: [...])),
],
)
Never call setState during build method.
Solution: Use Navigator API or defer to post-build callback.
For more errors and solutions, see Common Errors Reference.
flutter test
flutter test test/widget_test.dart
flutter test integration_test/
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
# Android
flutter test --platform android
# iOS
flutter test --platform ios
# Web
flutter test --platform chrome
flutter test --no-sound-null-safety test/my_test.dart
flutter test --verbose
flutter test --name "Counter value should be incremented"