Flutter Testing Guide: Unit, Widget, and Integration Tests
Flutter ships with three testing layers built into the SDK: unit tests (dart:test) for pure Dart logic, widget tests (flutter_test) for individual Flutter widgets in isolation, and integration tests (integration_test) for full app flows on a real device or emulator. This guide covers all three — setup, writing tests, and running them in CI.
Key Takeaways
Flutter has first-class testing support. No third-party test runner needed. flutter test runs unit and widget tests; flutter test integration_test/ runs integration tests.
Widget tests run in a test environment, not a real device. They're fast (seconds) and can test widget behavior, rendering, and interactions. They don't test platform channels or device-specific behavior.
Integration tests run on a real device or emulator. They're slow (minutes) but test real behavior including animations, platform channels, and system dialogs. Use them for critical flows only.
expect() in Flutter tests works like in Dart:test. For widget interactions, await tester.tap(), await tester.pump(), and expect(find.text('Hello'), findsOneWidget) are the core patterns.
Group tests with group(). This mirrors Jasmine/Jest describe. Use it to organize tests logically and get clear, hierarchical output.
Flutter Testing Stack
| Type | Package | Runs on | Speed | Use for |
|---|---|---|---|---|
| Unit tests | dart:test |
Dart VM | Fast (ms) | Business logic, utilities, models |
| Widget tests | flutter_test |
Flutter test env | Medium (seconds) | Widgets, state, UI interactions |
| Integration tests | integration_test |
Real device/emulator | Slow (minutes) | Full user flows, platform features |
Project Structure
lib/
models/
user.dart
services/
user_service.dart
widgets/
user_card.dart
test/
models/
user_test.dart # unit tests
services/
user_service_test.dart
widgets/
user_card_test.dart # widget tests
integration_test/
app_test.dart # integration tests
pubspec.yamlAdd test dependencies to pubspec.yaml:
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
mocktail: ^1.0.0
bloc_test: ^9.1.0Unit Tests
Unit tests test pure Dart code without Flutter widgets:
// lib/models/user.dart
class User {
final String id;
final String name;
final String email;
final UserRole role;
const User({
required this.id,
required this.name,
required this.email,
required this.role,
});
bool get isAdmin => role == UserRole.admin;
User copyWith({ String? name, String? email, UserRole? role }) {
return User(
id: id,
name: name ?? this.name,
email: email ?? this.email,
role: role ?? this.role,
);
}
@override
bool operator ==(Object other) =>
other is User && other.id == id;
@override
int get hashCode => id.hashCode;
}
enum UserRole { admin, editor, viewer }// test/models/user_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/models/user.dart';
void main() {
group('User', () {
final alice = User(
id: '1',
name: 'Alice',
email: 'alice@example.com',
role: UserRole.admin,
);
group('isAdmin', () {
test('returns true for admin role', () {
expect(alice.isAdmin, isTrue);
});
test('returns false for non-admin roles', () {
final viewer = alice.copyWith(role: UserRole.viewer);
expect(viewer.isAdmin, isFalse);
});
});
group('copyWith', () {
test('updates name while preserving other fields', () {
final updated = alice.copyWith(name: 'Alice Smith');
expect(updated.name, equals('Alice Smith'));
expect(updated.email, equals(alice.email));
expect(updated.id, equals(alice.id));
});
test('original is unchanged after copyWith', () {
alice.copyWith(name: 'Different Name');
expect(alice.name, equals('Alice'));
});
});
group('equality', () {
test('users with the same id are equal', () {
final alice2 = alice.copyWith(name: 'Alice Changed');
expect(alice, equals(alice2));
});
test('users with different ids are not equal', () {
final bob = User(id: '2', name: 'Bob', email: 'bob@example.com', role: UserRole.viewer);
expect(alice, isNot(equals(bob)));
});
});
});
}Widget Tests
Widget tests mount individual widgets in a test harness:
// lib/widgets/counter_widget.dart
import 'package:flutter/material.dart';
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count', key: const Key('count-text')),
ElevatedButton(
key: const Key('increment-btn'),
onPressed: () => setState(() => _count++),
child: const Text('+'),
),
ElevatedButton(
key: const Key('decrement-btn'),
onPressed: () => setState(() { if (_count > 0) _count--; }),
child: const Text('-'),
),
],
);
}
}// test/widgets/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('renders with initial count of 0', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(home: CounterWidget()),
);
expect(find.text('Count: 0'), findsOneWidget);
});
testWidgets('increments count on + button tap', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(home: CounterWidget()),
);
await tester.tap(find.byKey(const Key('increment-btn')));
await tester.pump(); // rebuild the widget
expect(find.text('Count: 1'), findsOneWidget);
});
testWidgets('does not decrement below 0', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(home: CounterWidget()),
);
await tester.tap(find.byKey(const Key('decrement-btn')));
await tester.pump();
expect(find.text('Count: 0'), findsOneWidget); // stays at 0
});
});
}Integration Tests
Integration tests run on a real device/emulator and test the full app:
// integration_test/app_test.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', () {
testWidgets('user can login and see the dashboard', (tester) async {
app.main();
await tester.pumpAndSettle();
// Find login form
await tester.enterText(find.byKey(const Key('email-field')), 'alice@example.com');
await tester.enterText(find.byKey(const Key('password-field')), 'password123');
await tester.tap(find.byKey(const Key('login-btn')));
await tester.pumpAndSettle();
// Should be on dashboard
expect(find.text('Dashboard'), findsOneWidget);
expect(find.text('Welcome, Alice'), findsOneWidget);
});
});
}Running Tests
# Unit and widget tests
flutter <span class="hljs-built_in">test
<span class="hljs-comment"># Specific test file
flutter <span class="hljs-built_in">test <span class="hljs-built_in">test/models/user_test.dart
<span class="hljs-comment"># With coverage
flutter <span class="hljs-built_in">test --coverage
genhtml coverage/lcov.info -o coverage/html
<span class="hljs-comment"># Integration tests (device must be connected)
flutter <span class="hljs-built_in">test integration_test/
<span class="hljs-comment"># Integration tests with specific driver
flutter drive --target=test_driver/app.dartWhat Each Level Tests
Unit tests are best for: business logic, model transformations, utility functions, pure Dart calculations.
Widget tests are best for: widget rendering, user interactions (taps, swipes), state changes, navigation between routes.
Integration tests are best for: critical user flows end-to-end, platform channel interactions, camera/location/notifications.
The rest of this cluster covers each layer in depth:
- Widget testing — WidgetTester, finders, pumping, async widgets
- Integration testing — integration_test package, Patrol for advanced automation
- Mocking with mocktail — dependency injection in Flutter tests
- BLoC testing — bloc_test package patterns
- Golden tests — screenshot comparison testing
HelpMeTest complements your Flutter test suite with 24/7 monitoring of your backend API and web services. Start free with 10 tests.