# Widget Testing Guide ## Overview Widget tests (component tests) verify that widgets render correctly and interact as expected. They run in a test environment that simulates widget lifecycle without a full UI system. ## When to Write Widget Tests - Verifying widget rendering - Testing user interactions (taps, drags, scrolling) - Validating widget state changes - Testing different orientations and screen sizes - Testing form inputs and validation ## Test Structure ### Basic Widget Test ```dart import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('widget description', (tester) async { // Build widget await tester.pumpWidget(const MyWidget()); // Find and verify expect(find.text('Hello'), findsOneWidget); }); } ``` ### Using MaterialApp ```dart testWidgets('widget with MaterialApp', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: MyWidget(), ), ), ); expect(find.byType(MyWidget), findsOneWidget); }); ``` ## Finding Widgets ### Common Finders ```dart // By text find.text('Hello') find.textContaining('ello') find.textRegExp(RegExp(r'\d+')) // By widget type find.byType(Text) find.byType(ElevatedButton) find.byType(MyWidget) // By key find.byKey(const Key('my-button')) find.byKey(const ValueKey('counter')) // By icon find.byIcon(Icons.add) // By widget instance find.byWidget(myWidgetInstance) // By specific widget ancestor find.ancestor( of: find.text('Child'), matching: find.byType(Column), ) ``` ### Finder Matchers ```dart // Find exactly one expect(find.text('Hello'), findsOneWidget); // Find multiple expect(find.text('Hello'), findsWidgets); // Find nothing expect(find.text('Missing'), findsNothing); // Find at least N expect(find.text('Item'), findsNWidgets(3)); ``` ## User Interactions ### Tapping ```dart // Tap a widget await tester.tap(find.byType(ElevatedButton)); // Trigger a frame await tester.pump(); // Wait for all animations await tester.pumpAndSettle(); ``` ### Dragging ```dart // Drag a widget await tester.drag( find.byType(MyWidget), const Offset(0, -300), ); await tester.pumpAndSettle(); ``` ### Entering Text ```dart // Enter text into TextField await tester.enterText( find.byType(TextField), 'Hello World', ); // Tap to focus, then enter await tester.tap(find.byType(TextField)); await tester.enterText(find.byType(TextField), 'Text'); // Enter by key await tester.enterText( find.byKey(const Key('email-field')), 'test@example.com', ); ``` ### Scrolling ```dart // Scroll a ListView await tester.fling( find.byType(ListView), const Offset(0, -500), 10000, ); await tester.pumpAndSettle(); // Scroll until widget is visible await tester.scrollUntilVisible( find.text('Target Item'), 500.0, ); ``` ### Long Press ```dart await tester.longPress(find.byType(IconButton)); await tester.pumpAndSettle(); ``` ## Testing Widget Properties ### Text Content ```dart testWidgets('displays correct text', (tester) async { await tester.pumpWidget(const MyWidget(text: 'Hello')); expect(find.text('Hello'), findsOneWidget); expect(find.text('Goodbye'), findsNothing); }); ``` ### Widget State ```dart testWidgets('updates state on button press', (tester) async { await tester.pumpWidget(const MyApp()); // Initial state expect(find.text('Count: 0'), findsOneWidget); // Tap increment button await tester.tap(find.byKey(const Key('increment'))); await tester.pumpAndSettle(); // Updated state expect(find.text('Count: 1'), findsOneWidget); }); ``` ### Conditional Rendering ```dart testWidgets('shows loading indicator when loading', (tester) async { await tester.pumpWidget(MyWidget(isLoading: true)); expect(find.byType(CircularProgressIndicator), findsOneWidget); expect(find.byType(ElevatedButton), findsNothing); }); testWidgets('shows button when not loading', (tester) async { await tester.pumpWidget(MyWidget(isLoading: false)); expect(find.byType(CircularProgressIndicator), findsNothing); expect(find.byType(ElevatedButton), findsOneWidget); }); ``` ## Testing with Arguments ```dart testWidgets('receives and displays title', (tester) async { const title = 'My Title'; await tester.pumpWidget( MaterialApp( home: MyWidget(title: title), ), ); expect(find.text(title), findsOneWidget); }); testWidgets('receives and uses callback', (tester) async { bool callbackCalled = false; await tester.pumpWidget( MaterialApp( home: MyWidget( onPressed: () => callbackCalled = true, ), ), ); await tester.tap(find.byType(ElevatedButton)); expect(callbackCalled, true); }); ``` ## Testing Orientation ```dart testWidgets('widget in portrait mode', (tester) async { await tester.binding.setSurfaceSize(const Size(400, 800)); await tester.pumpWidget(const MyApp()); // Verify portrait layout expect(find.byType(Column), findsOneWidget); // Clean up addTearDown(tester.binding.setSurfaceSize(null)); }); testWidgets('widget in landscape mode', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 400)); await tester.pumpWidget(const MyApp()); // Verify landscape layout expect(find.byType(Row), findsOneWidget); // Clean up addTearDown(tester.binding.setSurfaceSize(null)); }); ``` ## Testing Scrollable Widgets ### ListView Scrolling ```dart testWidgets('scrolling in ListView', (tester) async { await tester.pumpWidget( MaterialApp( home: ListView.builder( itemCount: 100, itemBuilder: (context, index) => ListTile( title: Text('Item $index'), ), ), ), ); // Initial item visible expect(find.text('Item 0'), findsOneWidget); expect(find.text('Item 50'), findsNothing); // Scroll down await tester.fling( find.byType(ListView), const Offset(0, -5000), 10000, ); await tester.pumpAndSettle(); // Later item now visible expect(find.text('Item 50'), findsOneWidget); }); ``` ### Scroll Until Visible ```dart testWidgets('scroll until specific item is visible', (tester) async { await tester.pumpWidget( MaterialApp( home: ListView.builder( itemCount: 100, itemBuilder: (context, index) => ListTile( key: ValueKey('item-$index'), title: Text('Item $index'), ), ), ), ); // Scroll until item 75 is visible await tester.scrollUntilVisible( find.byKey(const ValueKey('item-75')), 500.0, ); expect(find.text('Item 75'), findsOneWidget); }); ``` ## Testing Forms ### TextField Validation ```dart testWidgets('validates email field', (tester) async { final formKey = GlobalKey(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Form( key: formKey, child: Column( children: [ TextFormField( key: const Key('email'), validator: (value) { if (value == null || !value.contains('@')) { return 'Invalid email'; } return null; }, ), ElevatedButton( key: const Key('submit'), onPressed: () => formKey.currentState!.validate(), child: const Text('Submit'), ), ], ), ), ), ), ); // Enter invalid email await tester.enterText(find.byKey(const Key('email')), 'invalid'); await tester.tap(find.byKey(const Key('submit'))); await tester.pump(); // Should show error expect(find.text('Invalid email'), findsOneWidget); // Enter valid email await tester.enterText(find.byKey(const Key('email')), 'test@example.com'); await tester.tap(find.byKey(const Key('submit'))); await tester.pump(); // Should hide error expect(find.text('Invalid email'), findsNothing); }); ``` ### Multiple Form Fields ```dart testWidgets('form with multiple fields', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Form( child: Column( children: [ TextFormField( key: const Key('name'), decoration: const InputDecoration(labelText: 'Name'), ), TextFormField( key: const Key('email'), decoration: const InputDecoration(labelText: 'Email'), ), ElevatedButton( key: const Key('submit'), onPressed: () {}, child: const Text('Submit'), ), ], ), ), ), ), ); // Fill form await tester.enterText(find.byKey(const Key('name')), 'John Doe'); await tester.enterText(find.byKey(const Key('email')), 'john@example.com'); // Submit await tester.tap(find.byKey(const Key('submit'))); await tester.pumpAndSettle(); }); ``` ## Testing Animations ```dart testWidgets('animation plays correctly', (tester) async { await tester.pumpWidget(const AnimatedWidget()); // Initial state expect(find.byType(Opacity), findsOneWidget); // Trigger animation await tester.tap(find.byType(ElevatedButton)); // Pump animation frames await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 100)); // Animation complete await tester.pumpAndSettle(); // Verify final state final opacity = tester.widget(find.byType(Opacity)); expect(opacity.opacity, 0.0); }); ``` ## Testing Custom Widgets ### Testing InheritedWidgets ```dart class MyInheritedWidget extends InheritedWidget { final String data; const MyInheritedWidget({ super.key, required this.data, required Widget child, }) : super(child: child); static MyInheritedWidget of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType()!; } @override bool updateShouldNotify(MyInheritedWidget oldWidget) => data != oldWidget.data; } testWidgets('inherited widget provides data', (tester) async { await tester.pumpWidget( MyInheritedWidget( data: 'Test Data', child: Builder( builder: (context) { return Text(MyInheritedWidget.of(context).data); }, ), ), ); expect(find.text('Test Data'), findsOneWidget); }); ``` ### Testing StatefulWidget ```dart class CounterWidget extends StatefulWidget { @override State createState() => _CounterWidgetState(); } class _CounterWidgetState extends State { int _count = 0; void _increment() => setState(() => _count++); @override Widget build(BuildContext context) { return Column( children: [ Text('Count: $_count'), ElevatedButton( key: const Key('increment'), onPressed: _increment, child: const Text('Increment'), ), ], ); } } testWidgets('CounterWidget increments correctly', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: CounterWidget(), ), ), ); // Initial count expect(find.text('Count: 0'), findsOneWidget); // Increment await tester.tap(find.byKey(const Key('increment'))); await tester.pump(); expect(find.text('Count: 1'), findsOneWidget); }); ``` ## Testing with Mock Data ```dart class ProductList extends StatelessWidget { final List products; const ProductList({super.key, required this.products}); @override Widget build(BuildContext context) { return ListView.builder( itemCount: products.length, itemBuilder: (context, index) => ListTile( title: Text(products[index].name), subtitle: Text('\$${products[index].price}'), ), ); } } testWidgets('displays list of products', (tester) async { final products = [ Product(name: 'Product 1', price: 10.99), Product(name: 'Product 2', price: 20.99), ]; await tester.pumpWidget( MaterialApp( home: Scaffold( body: ProductList(products: products), ), ), ); expect(find.text('Product 1'), findsOneWidget); expect(find.text('\$10.99'), findsOneWidget); expect(find.text('Product 2'), findsOneWidget); expect(find.text('\$20.99'), findsOneWidget); }); ``` ## Testing Accessibility ```dart testWidgets('widget has proper semantics', (tester) async { await tester.pumpWidget( MaterialApp( home: Semantics( label: 'Submit button', button: true, child: ElevatedButton( onPressed: () {}, child: const Text('Submit'), ), ), ), ); final semantics = tester.getSemantics(find.byType(ElevatedButton)); expect(semantics.label, 'Submit button'); expect(semantics.hasAction(SemanticsAction.tap), true); }); ``` ## Best Practices 1. **Use descriptive test names** - Explain what and why 2. **Keep tests independent** - Each test should be standalone 3. **Use pumpAndSettle()** - Wait for all animations 4. **Test user behavior, not implementation** - Focus on what users see and do 5. **Group related tests** - Use `group()` for organization 6. **Test edge cases** - Empty lists, null values, errors 7. **Use keys for widgets** - Makes finding easier and more reliable 8. **Test accessibility** - Verify semantic labels and actions ## Common Patterns ### Testing Navigation ```dart testWidgets('navigates to detail screen', (tester) async { await tester.pumpWidget( MaterialApp( initialRoute: '/', routes: { '/': (context) => const HomeScreen(), '/detail': (context) => const DetailScreen(), }, ), ); // Tap navigation button await tester.tap(find.text('Go to Detail')); await tester.pumpAndSettle(); // Verify navigation expect(find.text('Detail Screen'), findsOneWidget); expect(find.text('Home Screen'), findsNothing); }); ``` ### Testing Loading States ```dart testWidgets('shows loading then content', (tester) async { bool isLoading = true; await tester.pumpWidget( MaterialApp( home: StatefulBuilder( builder: (context, setState) { return isLoading ? const CircularProgressIndicator() : const Text('Content Loaded'); }, ), ), ); expect(find.byType(CircularProgressIndicator), findsOneWidget); // Simulate data load isLoading = false; await tester.pump(); expect(find.byType(CircularProgressIndicator), findsNothing); expect(find.text('Content Loaded'), findsOneWidget); }); ```