Widget testing in Flutter
By Ann Tech · 23 February 2025
Widget tests verify that your UI renders correctly and responds to interactions — without running on a device. They're faster than integration tests and more realistic than unit tests.
Setup
No extra dependencies needed — flutter_test is included with the Flutter SDK.
// test/widgets/product_card_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/product_card.dart';
void main() {
testWidgets('shows product name and price', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: ProductCard(
product: Product(
id: '1',
name: 'Flutter Hat',
price: 29.99,
imageUrl: 'https://example.com/hat.png',
),
),
),
);
expect(find.text('Flutter Hat'), findsOneWidget);
expect(find.text('\$29.99'), findsOneWidget);
});
}
flutter test test/widgets/product_card_test.dart
The pump methods
// Pump one frame
await tester.pump();
// Pump until no more frames are pending (animations complete, FutureBuilders resolve)
await tester.pumpAndSettle();
// Pump a specific duration (useful for timed animations)
await tester.pump(const Duration(milliseconds: 500));
Finders
find.text('Submit') // By display text
find.byKey(const Key('submit')) // By Key
find.byType(ElevatedButton) // By widget type
find.byIcon(Icons.add) // By icon
find.byTooltip('Add item') // By tooltip string
find.descendant(
of: find.byKey(const Key('cart')),
matching: find.byType(IconButton),
) // Scoped search
Matchers
expect(find.text('Hello'), findsOneWidget); // Exactly one
expect(find.text('Item'), findsNWidgets(3)); // Exactly three
expect(find.text('Ghost'), findsNothing); // Zero
expect(find.text('Button'), findsWidgets); // One or more
Testing interactions
testWidgets('add to cart button triggers callback', (tester) async {
var addedProductId = '';
await tester.pumpWidget(
MaterialApp(
home: ProductCard(
product: testProduct,
onAddToCart: (id) => addedProductId = id,
),
),
);
await tester.tap(find.byKey(const Key('add_to_cart_button')));
await tester.pump();
expect(addedProductId, equals(testProduct.id));
});
testWidgets('text field validates email format', (tester) async {
await tester.pumpWidget(const MaterialApp(home: LoginScreen()));
await tester.enterText(find.byKey(const Key('email_field')), 'not-an-email');
await tester.tap(find.byKey(const Key('submit_button')));
await tester.pump();
expect(find.text('Enter a valid email'), findsOneWidget);
});
Testing with Riverpod
testWidgets('cart shows item count', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
cartNotifierProvider.overrideWith(() {
return CartNotifier()..state = Cart(
items: [CartItem(product: testProduct, quantity: 2)],
);
}),
],
child: const MaterialApp(home: CartBadge()),
),
);
expect(find.text('2'), findsOneWidget);
});
Testing async widgets
testWidgets('shows loading then content', (tester) async {
final completer = Completer<List<Product>>();
await tester.pumpWidget(
ProviderScope(
overrides: [
productsProvider.overrideWith((_) => completer.future),
],
child: const MaterialApp(home: ProductsScreen()),
),
);
// Before future resolves
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// Resolve the future
completer.complete([testProduct]);
await tester.pumpAndSettle();
// After future resolves
expect(find.byType(CircularProgressIndicator), findsNothing);
expect(find.text(testProduct.name), findsOneWidget);
});
Testing with BLoC
testWidgets('error state shows retry button', (tester) async {
final bloc = MockProductsBloc();
whenListen(
bloc,
Stream.value(ProductsError('Network error')),
initialState: ProductsLoading(),
);
await tester.pumpWidget(
BlocProvider<ProductsBloc>.value(
value: bloc,
child: const MaterialApp(home: ProductsScreen()),
),
);
await tester.pumpAndSettle();
expect(find.text('Network error'), findsOneWidget);
expect(find.text('Retry'), findsOneWidget);
});
Golden tests
Golden tests compare widget screenshots to stored reference images:
testWidgets('ProductCard matches golden', (tester) async {
await tester.pumpWidget(
MaterialApp(home: ProductCard(product: testProduct)),
);
await expectLater(
find.byType(ProductCard),
matchesGoldenFile('goldens/product_card.png'),
);
});
// Generate goldens:
// flutter test --update-goldens
// Check in the generated images
Common pitfalls
Not calling pumpAndSettle after async operations. If a widget triggers a network call (even a mocked one), you need to await tester.pumpAndSettle() to let the FutureBuilder rebuild with the result.
Wrapping test widgets in Scaffold when not needed. Some widgets (Text, Container) work without Scaffold. But widgets that use Theme, MediaQuery, Navigator, or Scaffold.of(context) need to be wrapped in MaterialApp. When in doubt, wrap in MaterialApp.
Testing implementation details. Tests that check whether a specific private method was called are brittle. Test visible behavior: the text that appears, the navigation that happens, the callback that fires.
Sign in to like, dislike, or report.