Flutter has excellent built-in testing support at three levels: unit, widget, and integration tests.
Unit Tests
Dart
// test/models/user_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/models/user.dart';
void main() {
group('User', () {
test('fromJson creates valid User', () {
final json = {
'id': '1',
'name': 'Alice',
'email': 'alice@example.com',
'created_at': '2024-01-01T00:00:00Z',
};
final user = User.fromJson(json);
expect(user.id, '1');
expect(user.name, 'Alice');
expect(user.email, 'alice@example.com');
});
test('toJson returns correct map', () {
final user = User(
id: '1',
name: 'Alice',
email: 'alice@example.com',
createdAt: DateTime(2024),
);
final json = user.toJson();
expect(json['name'], 'Alice');
expect(json['email'], 'alice@example.com');
expect(json.containsKey('id'), false); // Not sent in POST
});
test('copyWith preserves unchanged fields', () {
final user = User(
id: '1',
name: 'Alice',
email: 'alice@example.com',
createdAt: DateTime(2024),
);
final updated = user.copyWith(name: 'Bob');
expect(updated.name, 'Bob');
expect(updated.email, 'alice@example.com');
expect(updated.id, '1');
});
});
}Widget Tests
Dart
// test/widgets/counter_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/counter.dart';
void main() {
group('Counter Widget', () {
testWidgets('displays initial count of 0', (tester) async {
await tester.pumpWidget(MaterialApp(home: Counter()));
expect(find.text('Count: 0'), findsOneWidget);
expect(find.text('Count: 1'), findsNothing);
});
testWidgets('increments count when button is tapped', (tester) async {
await tester.pumpWidget(MaterialApp(home: Counter()));
// Find and tap the button
await tester.tap(find.byIcon(Icons.add));
await tester.pump(); // Rebuild after setState
expect(find.text('Count: 1'), findsOneWidget);
});
testWidgets('increments multiple times', (tester) async {
await tester.pumpWidget(MaterialApp(home: Counter()));
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('Count: 3'), findsOneWidget);
});
testWidgets('has an add button', (tester) async {
await tester.pumpWidget(MaterialApp(home: Counter()));
expect(find.byIcon(Icons.add), findsOneWidget);
});
});
}Testing Forms
Dart
testWidgets('login form validates empty fields', (tester) async {
await tester.pumpWidget(MaterialApp(home: LoginScreen()));
// Submit without filling in fields
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// Should show validation errors
expect(find.text('Email is required'), findsOneWidget);
expect(find.text('Password is required'), findsOneWidget);
});
testWidgets('login form submits valid data', (tester) async {
await tester.pumpWidget(MaterialApp(home: LoginScreen()));
// Fill in fields
await tester.enterText(find.byKey(Key('email')), 'test@example.com');
await tester.enterText(find.byKey(Key('password')), 'password123');
// Submit
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle(); // Wait for async operations
// Should navigate to home
expect(find.byType(HomeScreen), findsOneWidget);
});Mocking
Dart
// test/services/user_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:my_app/services/api_service.dart';
@GenerateMocks([http.Client])
import 'user_service_test.mocks.dart';
void main() {
group('ApiService', () {
late MockClient mockClient;
late ApiService apiService;
setUp(() {
mockClient = MockClient();
apiService = ApiService(client: mockClient);
});
test('getUsers returns list of users on 200', () async {
when(mockClient.get(any, headers: anyNamed('headers'))).thenAnswer(
(_) async => http.Response(
'[{"id":"1","name":"Alice","email":"alice@test.com","created_at":"2024-01-01"}]',
200,
),
);
final users = await apiService.getUsers();
expect(users.length, 1);
expect(users[0].name, 'Alice');
});
test('getUsers throws on non-200', () async {
when(mockClient.get(any, headers: anyNamed('headers'))).thenAnswer(
(_) async => http.Response('Not Found', 404),
);
expect(() => apiService.getUsers(), throwsA(isA<ApiException>()));
});
});
}Integration Tests
Dart
// 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();
testWidgets('full login flow', (tester) async {
app.main();
await tester.pumpAndSettle();
// Verify we start on login screen
expect(find.text('Login'), findsOneWidget);
// Enter credentials
await tester.enterText(find.byKey(Key('email')), 'test@example.com');
await tester.enterText(find.byKey(Key('password')), 'password123');
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
// Should be on home screen
expect(find.text('Welcome'), findsOneWidget);
});
}Running Tests
Bash
# Unit and widget tests
flutter test
# With coverage
flutter test --coverage
# Specific file
flutter test test/models/user_test.dart
# Watch mode
flutter test --watch
# Integration tests (needs device/emulator)
flutter test integration_test/
# Generate mock files
dart run build_runner buildTest Directory Structure
Code
test/
├── models/
│ ├── user_test.dart
│ └── post_test.dart
├── services/
│ ├── api_service_test.dart
│ └── auth_service_test.dart
├── widgets/
│ ├── counter_test.dart
│ └── login_form_test.dart
└── helpers/
└── test_helpers.dart
integration_test/
└── app_test.dart💡 Tip: Aim for 80%+ unit test coverage. Widget tests should cover all interactive elements. Integration tests are for critical user flows only.