12 KiB
Common Testing Errors
Overview
This guide covers frequently encountered Flutter testing errors and their solutions.
Layout Errors
'A RenderFlex overflowed...'
Error Message:
The following assertion was thrown during layout:
A RenderFlex overflowed by 1146 pixels on the right.
Cause: Yellow and black stripes indicate overflow when a child widget is too large for its parent (Row/Column).
Solution: Wrap the overflowing widget in Expanded or Flexible.
// Problem
Row(
children: [
Icon(Icons.message),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Title', style: Theme.of(context).textTheme.headlineMedium),
Text('Very long text that overflows the available space...'),
],
),
],
)
// Solution
Row(
children: [
Icon(Icons.message),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Title', style: Theme.of(context).textTheme.headlineMedium),
Text('Very long text that fits in the available space...'),
],
),
),
],
)
'Vertical viewport was given unbounded height'
Error Message:
Vertical viewport was given unbounded height.
Viewports expand in the scrolling direction to fill their container.
This situation typically happens when a scrollable widget is nested inside another scrollable widget.
Cause: ListView or other scrollable widget inside Column without height constraints.
Solution: Wrap in Expanded or use shrinkWrap: true.
// Problem
Column(
children: [
Text('Header'),
ListView(
children: [
ListTile(leading: Icon(Icons.map), title: Text('Map')),
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
],
),
],
)
// Solution 1: Expanded
Column(
children: [
Text('Header'),
Expanded(
child: ListView(
children: [
ListTile(leading: Icon(Icons.map), title: Text('Map')),
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
],
),
),
],
)
// Solution 2: shrinkWrap
Column(
children: [
Text('Header'),
ListView(
shrinkWrap: true,
children: [
ListTile(leading: Icon(Icons.map), title: Text('Map')),
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
],
),
],
)
'An InputDecorator...cannot have an unbounded width'
Error Message:
An InputDecorator, which is typically created by a TextField, cannot have an unbounded width.
This happens when the parent widget does not provide a finite width constraint.
Cause: TextField or TextFormField inside Row without width constraints.
Solution: Wrap in Expanded or SizedBox.
// Problem
Row(
children: [TextField()],
)
// Solution 1: Expanded
Row(
children: [Expanded(child: TextFormField())],
)
// Solution 2: SizedBox
Row(
children: [SizedBox(width: 200, child: TextFormField())],
)
Widget Lifecycle Errors
'setState called during build'
Error Message:
setState() or markNeedsBuild() called during build.
This Overlay widget cannot be marked as needing to build because the framework
is already in the process of building widgets.
Cause: Calling setState (or methods that call it like showDialog) directly in the build method.
Solution: Use WidgetsBinding.instance.addPostFrameCallback or Navigator API.
// Problem
Widget build(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(title: Text('Error!')),
);
return Center(child: Text('Show Dialog'));
}
// Solution 1: Post-frame callback
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (context) => AlertDialog(title: Text('Success!')),
);
});
return Center(child: Text('Show Dialog'));
}
// Solution 2: Navigator API (for initial navigation)
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => DialogScreen(),
),
);
},
child: Text('Show Dialog'),
);
}
Parent Widget Errors
'Incorrect use of ParentData widget'
Error Message:
The following assertion was thrown while looking for parent data:
Incorrect use of ParentDataWidget.
Usually, this indicates that at least one of the offending ParentDataWidgets
listed above is not placed directly inside a compatible ancestor widget.
Cause: Using widgets that require specific parent widgets in wrong context.
Common Solutions:
| Widget | Expected Parent | Solution |
|---|---|---|
Flexible |
Row, Column, Flex |
Ensure parent is Row/Column/Flex |
Expanded |
Row, Column, Flex |
Ensure parent is Row/Column/Flex |
Positioned |
Stack |
Wrap in Stack |
TableCell |
Table |
Ensure parent is Table |
// Problem
Column(
children: [
Expanded(child: Text('Wrong!')), // Expanded needs Row/Column parent, not Column children
],
)
// Solution
Column(
children: [
Row(
children: [
Expanded(child: Text('Correct!')),
],
),
],
)
Testing-Specific Errors
'WidgetTester.pumpWidget() called with a widget that doesn't include a MaterialApp'
Error Message:
WidgetTester.pumpWidget() called with a widget that doesn't include a MaterialApp.
Cause: Pumping widget without MaterialApp context.
Solution: Wrap widget in MaterialApp.
// Problem
testWidgets('test without MaterialApp', (tester) async {
await tester.pumpWidget(MyWidget());
});
// Solution
testWidgets('test with MaterialApp', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: MyWidget(),
),
);
});
'No Finder found'
Error Message:
No Finder found.
Test failed: No elements found matching the finder.
Cause: Widget not found in tree at time of test.
Common Solutions:
// 1. Wrong text/content
expect(find.text('Wrong Text'), findsNothing); // Error
// Solution: Use correct text
expect(find.text('Correct Text'), findsOneWidget);
// 2. Widget not built yet
testWidgets('widget not built', (tester) async {
await tester.pumpWidget(MyWidget());
expect(find.text('Async Text'), findsOneWidget); // Error
});
// Solution: Pump and settle for async operations
testWidgets('widget not built', (tester) async {
await tester.pumpWidget(MyWidget());
await tester.pumpAndSettle(); // Wait for async operations
expect(find.text('Async Text'), findsOneWidget);
});
// 3. Wrong finder type
testWidgets('wrong finder', (tester) async {
await tester.pumpWidget(TextButton(child: Text('Button')));
await tester.tap(find.byType(ElevatedButton)); // Error
});
// Solution: Use correct type or use Text finder
testWidgets('correct finder', (tester) async {
await tester.pumpWidget(TextButton(child: Text('Button')));
await tester.tap(find.byType(TextButton)); // Correct
});
Plugin/Platform Errors
'MissingPluginException'
Error Message:
MissingPluginException(No implementation found for method MethodName on channel channel.name)
Cause: Plugin not registered or mock not set up in tests.
Solution: Mock the method channel in tests.
import 'package:flutter/services.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('your.plugin.channel'),
(MethodCall methodCall) async {
if (methodCall.method == 'getPlatformVersion') {
return 'Android 12';
}
throw MissingPluginException();
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('your.plugin.channel'),
null,
);
});
}
Async Errors
'TimeoutException'
Error Message:
TimeoutException after 0:00:05.000000: Test timed out after 5 seconds.
Cause: Async operation not completing or pumpAndSettle() waiting indefinitely.
Solution: Set timeout or fix infinite loops.
// Solution 1: Increase timeout
testWidgets(
'long-running test',
(tester) async {
await tester.pumpWidget(MyWidget());
await Future.delayed(const Duration(seconds: 10));
},
timeout: const Timeout(Duration(minutes: 1)),
);
// Solution 2: Fix infinite pumpAndSettle
testWidgets('fix infinite settle', (tester) async {
await tester.pumpWidget(MyWidget());
await tester.pump(); // Use pump() instead of pumpAndSettle() if animation loops
});
Data Errors
'RangeError'
Error Message:
RangeError: Index out of range: index should be less than 5, but is 5
Cause: Accessing list/array with invalid index.
Solution: Check bounds before access or handle empty cases.
// Problem
testWidgets('range error', (tester) async {
final items = [1, 2, 3, 4, 5];
expect(items[5], 6); // Error: index 5 out of range
});
// Solution 1: Check bounds
testWidgets('safe access', (tester) async {
final items = [1, 2, 3, 4, 5];
if (items.length > 5) {
expect(items[5], 6);
}
});
// Solution 2: Handle empty lists
testWidgets('handle empty list', (tester) async {
final items = <int>[];
if (items.isEmpty) {
expect(find.text('No items'), findsOneWidget);
} else {
expect(items.first, 1);
}
});
Debugging Tips
Enable Verbose Logging
flutter test --verbose
Take Screenshots in Integration Tests
import 'package:integration_test/integration_test.dart';
testWidgets('debug with screenshot', (tester) async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
await tester.pumpWidget(MyWidget());
await binding.convertFlutterSurfaceToImage();
await tester.pump();
await binding.takeScreenshot('debug-state');
});
Print Widget Tree
testWidgets('print tree', (tester) async {
await tester.pumpWidget(MyWidget());
debugDumpApp();
});
Use Debug Flags
testWidgets('debug flags', (tester) async {
debugPrint('Current state: ...');
await tester.pumpWidget(MyWidget());
debugPrint('Widget tree: ${tester.widgetList(find.byType(MyWidget))}');
});
Prevention
Use Type Safety
// Good: Use type-safe access
final text = tester.widget<Text>(find.byType(Text));
// Avoid: Unsafe casts
final text = find.byType(Text).evaluate().first.widget as Text;
Check Before Actions
testWidgets('safe tap', (tester) async {
await tester.pumpWidget(MyWidget());
final button = find.byType(ElevatedButton);
if (button.evaluate().isNotEmpty) {
await tester.tap(button);
} else {
fail('Button not found');
}
});
Handle All Cases
testWidgets('comprehensive', (tester) async {
await tester.pumpWidget(MyWidget());
// Check all possible states
if (find.text('Loading').evaluate().isNotEmpty) {
// Handle loading state
} else if (find.text('Error').evaluate().isNotEmpty) {
// Handle error state
} else {
// Handle success state
}
});