Answer
Overview
Flutter supports three main types of testing to ensure app quality: Unit Testing, Widget Testing, and Integration Testing. Each type tests different aspects of your application.
1. Unit Testing
Purpose: Test individual functions, methods, or classes in isolation.
What to test:
- Business logic
- Data models
- Utility functions
- Repositories
- Services
Example
dart// lib/utils/calculator.dart class Calculator { int add(int a, int b) => a + b; int subtract(int a, int b) => a - b; int multiply(int a, int b) => a * b; double divide(int a, int b) { if (b == 0) throw ArgumentError('Cannot divide by zero'); return a / b; } }
dart// test/calculator_test.dart import 'package:flutter_test/flutter_test.dart'; import 'package:my_app/utils/calculator.dart'; void main() { group('Calculator', () { late Calculator calculator; setUp(() { calculator = Calculator(); }); test('add returns sum of two numbers', () { expect(calculator.add(2, 3), 5); expect(calculator.add(-1, 1), 0); }); test('subtract returns difference', () { expect(calculator.subtract(5, 3), 2); }); test('multiply returns product', () { expect(calculator.multiply(3, 4), 12); }); test('divide returns quotient', () { expect(calculator.divide(10, 2), 5.0); }); test('divide by zero throws error', () { expect(() => calculator.divide(10, 0), throwsArgumentError); }); }); }
Running Unit Tests
bashflutter test test/calculator_test.dart
2. Widget Testing
Purpose: Test UI components (widgets) and their interactions.
What to test:
- Widget rendering
- User interactions (tap, swipe, input)
- Widget state changes
- Widget tree structure
Example
dart// lib/widgets/counter_widget.dart class CounterWidget extends StatefulWidget { _CounterWidgetState createState() => _CounterWidgetState(); } class _CounterWidgetState extends State<CounterWidget> { int _counter = 0; void _increment() => setState(() => _counter++); void _decrement() => setState(() => _counter--); Widget build(BuildContext context) { return Column( children: [ Text('$_counter', key: Key('counter_text')), ElevatedButton( key: Key('increment_button'), onPressed: _increment, child: Text('Increment'), ), ElevatedButton( key: Key('decrement_button'), onPressed: _decrement, child: Text('Decrement'), ), ], ); } }
dart// test/widget/counter_widget_test.dart import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:my_app/widgets/counter_widget.dart'; void main() { group('CounterWidget', () { testWidgets('displays initial counter value of 0', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(home: CounterWidget())); expect(find.text('0'), findsOneWidget); }); testWidgets('increments counter when increment button is tapped', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(home: CounterWidget())); // Tap increment button await tester.tap(find.byKey(Key('increment_button'))); await tester.pump(); expect(find.text('1'), findsOneWidget); }); testWidgets('decrements counter when decrement button is tapped', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(home: CounterWidget())); // Increment first await tester.tap(find.byKey(Key('increment_button'))); await tester.pump(); // Then decrement await tester.tap(find.byKey(Key('decrement_button'))); await tester.pump(); expect(find.text('0'), findsOneWidget); }); testWidgets('finds widgets by type', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(home: CounterWidget())); expect(find.byType(ElevatedButton), findsNWidgets(2)); expect(find.byType(Text), findsNWidgets(3)); // counter + 2 button labels }); }); }
Common Widget Test Methods
dart// Finding widgets find.text('Hello'); find.byKey(Key('my_key')); find.byType(ElevatedButton); find.byIcon(Icons.add); find.byWidget(myWidget); // Interactions await tester.tap(find.byKey(Key('button'))); await tester.enterText(find.byType(TextField), 'Hello'); await tester.drag(find.byType(ListView), Offset(0, -200)); // Pump methods await tester.pump(); // Trigger a frame await tester.pumpAndSettle(); // Wait for all animations to complete await tester.pumpWidget(widget); // Render widget // Assertions expect(find.text('Hello'), findsOneWidget); expect(find.byType(Text), findsNWidgets(3)); expect(find.text('Missing'), findsNothing);
Running Widget Tests
bashflutter test test/widget/counter_widget_test.dart
3. Integration Testing
Purpose: Test complete app or large parts working together (end-to-end testing).
What to test:
- User flows (login → dashboard → logout)
- Navigation between screens
- Real device/emulator behavior
- Performance
- Interactions with backend APIs
Setup
yaml# pubspec.yaml dev_dependencies: integration_test: sdk: flutter flutter_test: sdk: flutter
Example
dart// integration_test/app_test.dart import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:my_app/main.dart' as app; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('App Integration Test', () { testWidgets('complete user flow', (WidgetTester tester) async { // Start app app.main(); await tester.pumpAndSettle(); // 1. Verify home page expect(find.text('Welcome'), findsOneWidget); // 2. Navigate to login await tester.tap(find.byKey(Key('login_button'))); await tester.pumpAndSettle(); // 3. Enter credentials await tester.enterText(find.byKey(Key('email_field')), 'test@example.com'); await tester.enterText(find.byKey(Key('password_field')), 'password123'); // 4. Submit login await tester.tap(find.byKey(Key('submit_button'))); await tester.pumpAndSettle(Duration(seconds: 3)); // Wait for API // 5. Verify dashboard expect(find.text('Dashboard'), findsOneWidget); // 6. Logout await tester.tap(find.byIcon(Icons.logout)); await tester.pumpAndSettle(); // 7. Verify back to home expect(find.text('Welcome'), findsOneWidget); }); testWidgets('navigation test', (WidgetTester tester) async { app.main(); await tester.pumpAndSettle(); // Navigate through multiple screens await tester.tap(find.text('Settings')); await tester.pumpAndSettle(); expect(find.text('Settings Page'), findsOneWidget); await tester.tap(find.byIcon(Icons.arrow_back)); await tester.pumpAndSettle(); expect(find.text('Welcome'), findsOneWidget); }); }); }
Running Integration Tests
bash# On emulator/device flutter test integration_test/app_test.dart # On specific device flutter test integration_test/app_test.dart -d <device_id> # With debugging flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart
Comparison Table
| Feature | Unit Test | Widget Test | Integration Test |
|---|---|---|---|
| Tests | Functions, classes | UI widgets | Full app flow |
| Speed | Very fast | Fast | Slow |
| Dependencies | Isolated (mocked) | Mocked services | Real services |
| Runs on | Local machine | Local machine | Device/emulator |
| Coverage | Business logic | UI components | End-to-end |
| Use case | Validate calculations | Validate UI | Validate user flows |
| Package | text | text | text |
Test Pyramid
text/ / \ Integration Tests (Few, Slow, Expensive) /---- / \ Widget Tests (Moderate, Moderate Speed) /-------- /__________\ Unit Tests (Many, Fast, Cheap)
Best Practice: Write more unit tests, fewer integration tests.
Additional Test Types
4. Golden Tests (Snapshot Testing)
Test that UI renders exactly as expected by comparing screenshots.
darttestWidgets('golden test', (WidgetTester tester) async { await tester.pumpWidget(MyWidget()); await expectLater( find.byType(MyWidget), matchesGoldenFile('goldens/my_widget.png'), ); });
5. Performance Testing
dartimport 'package:flutter_driver/flutter_driver.dart'; void main() { group('Performance Test', () { late FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); }); tearDownAll(() async { driver.close(); }); test('scroll performance', () async { final timeline = await driver.traceAction(() async { await driver.scroll( find.byType('ListView'), 0, -500, Duration(milliseconds: 300), ); }); final summary = TimelineSummary.summarize(timeline); summary.writeSummaryToFile('scroll_performance', pretty: true); }); }); }
Best Practices
| Practice | Recommendation |
|---|---|
| Test coverage | Aim for 80%+ code coverage |
| Test naming | Use descriptive names: text |
| setUp/tearDown | Use for common initialization/cleanup |
| Mock dependencies | Use text |
| Organize tests | Mirror lib/ structure in test/ |
| Run frequently | Run tests before every commit |
Running All Tests
bash# Run all tests flutter test # Run with coverage flutter test --coverage # Run specific file flutter test test/my_test.dart # Run tests in watch mode flutter test --watch