Тестирование — критическая часть разработки Flutter приложений. В этом руководстве разберём все виды тестов: unit, widget, integration.
Виды тестов во Flutter
Flutter поддерживает три типа тестов:
- Unit тесты — тестирование отдельных функций и классов
- Widget тесты — тестирование UI компонентов
- Integration тесты — тестирование всего приложения
Unit тесты
Базовый пример
import 'package:flutter_test/flutter_test.dart';
void main() {
test('Counter increments', () {
final counter = Counter();
counter.increment();
expect(counter.value, 1);
});
}
Тестирование с Matchers
test('User validation', () {
final user = User(name: 'John', email: 'john@example.com');
expect(user.name, equals('John'));
expect(user.email, contains('@'));
expect(user.isValid, isTrue);
});
Группировка тестов
group('User', () {
test('isValid returns true for valid user', () {
final user = User(name: 'John', email: 'john@example.com');
expect(user.isValid, isTrue);
});
test('isValid returns false for invalid email', () {
final user = User(name: 'John', email: 'invalid');
expect(user.isValid, isFalse);
});
});
Mocking с Mockito
Установка
dev_dependencies:
mockito: ^5.4.0
build_runner: ^2.4.0
Создание Mock классов
import 'package:mockito/mockito.dart';
class MockAuthService extends Mock implements AuthService {}
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
Использование в тестах
test('Login succeeds with valid credentials', () async {
final mockAuthService = MockAuthService();
when(mockAuthService.login('user@example.com', 'password'))
.thenAnswer((_) async => User(id: '1', name: 'John'));
final result = await mockAuthService.login('user@example.com', 'password');
expect(result.name, equals('John'));
verify(mockAuthService.login('user@example.com', 'password')).called(1);
});
Verify взаимодействий
test('Logout calls auth service', () {
final mockAuth = MockAuthService();
final viewModel = LoginViewModel(mockAuth);
viewModel.logout();
verify(mockAuth.logout()).called(1);
});
Widget тесты
Базовый widget тест
testWidgets('MyWidget displays text', (tester) async {
await tester.pumpWidget(MyWidget());
expect(find.text('Hello'), findsOneWidget);
});
Тестирование взаимодействий
testWidgets('Button increments counter', (tester) async {
await tester.pumpWidget(MyApp());
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byType(FloatingActionButton));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
Поиск виджетов
// По тексту
find.text('Submit')
// По типу
find.byType(ElevatedButton)
// По ключу
find.byKey(Key('submit-button'))
// по иконке
find.byIcon(Icons.send)
Работа с TextField
testWidgets('TextField input works', (tester) async {
await tester.pumpWidget(MyWidget());
final textField = find.byType(TextField);
await tester.enterText(textField, 'Hello World');
await tester.pump();
expect(find.text('Hello World'), findsOneWidget);
});
Тестирование ListView
testWidgets('ListView displays items', (tester) async {
final items = ['Item 1', 'Item 2', 'Item 3'];
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => Text(items[index]),
),
),
),
);
for (final item in items) {
expect(find.text(item), findsOneWidget);
}
});
Тестирование с Provider
testWidgets('Counter increments with Provider', (tester) async {
await tester.pumpWidget(
Provider<Counter>(
create: (_) => Counter(),
child: const MaterialApp(home: CounterPage()),
),
);
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
Integration тесты
Настройка
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Full app flow', (tester) async {
await tester.pumpWidget(MyApp());
// Тест полного пользовательского пути
});
}
Пример сценария
testWidgets('Complete login flow', (tester) async {
await tester.pumpWidget(MyApp());
// Ввод email
await tester.enterText(find.byKey(Key('email-field')), 'user@example.com');
// Ввод пароля
await tester.enterText(find.byKey(Key('password-field')), 'password123');
// Тап на кнопку
await tester.tap(find.byKey(Key('login-button')));
await tester.pumpAndSettle();
// Проверка результата
expect(find.text('Welcome'), findsOneWidget);
});
Golden тесты
Создание golden файла
testWidgets('MyWidget golden test', (tester) async {
await tester.pumpWidget(MyWidget());
await expectLater(
find.byType(MyWidget),
matchesGoldenFile('goldens/my_widget.png'),
);
});
Обновление golden файлов
flutter test --update-goldens
Best Practices
1. Organize тесты
// unit/auth_service_test.dart
// widgets/login_page_test.dart
// integration/login_flow_test.dart
2. Используйте describe/it/group
group('AuthService', () {
group('login', () {
test('succeeds with valid credentials', () {});
test('fails with invalid credentials', () {});
});
});
3. Тестируйте edge cases
test('handles empty list', () {
final result = processItems([]);
expect(result, isEmpty);
});
test('handles null input', () {
final result = processItems(null);
expect(result, isNull);
});
4. Изолируйте тесты
setUp(() {
// Код перед каждым тестом
});
tearDown(() {
// Код после каждого теста
});
setUpAll(() {
// Код перед всеми тестами
});
tearDownAll(() {
// Код после всех тестов
});
Покрытие кода
Запуск с покрытием
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
Заключение
Тестирование во Flutter в 2025 — это обязательная практика. Используйте все три типа тестов для надёжных приложений.