Что использовал для написания widget-test?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Widget Testing в Flutter
Widget testing (также называется component testing) — это тестирование отдельных виджетов и их поведения в изолированной среде. На практике я использовал несколько инструментов и подходов для написания качественных widget-тестов.
Основные инструменты
1. flutter_test пакет
flutter_test — встроенный пакет для тестирования. Используется для всех типов тестов: unit, widget, integration.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
void main() {
group('CounterButton Widget Tests', () {
testWidgets('displays initial counter value', (WidgetTester tester) async {
// Arrange
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: CounterButton(initialValue: 5),
),
),
);
// Act & Assert
expect(find.text('5'), findsOneWidget);
});
testWidgets('increments counter on button press', (WidgetTester tester) async {
// Arrange
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: CounterButton(initialValue: 0),
),
),
);
// Act
await tester.tap(find.byIcon(Icons.add));
await tester.pump(); // Перестроить виджет
// Assert
expect(find.text('1'), findsOneWidget);
});
});
}
2. mockito для моков
mockito — пакет для создания mock объектов. Используется для моков зависимостей (сервисы, репозитории).
import 'package:mockito/mockito.dart';
class MockUserService extends Mock implements UserService {}
void main() {
testWidgets('displays user data', (WidgetTester tester) async {
final mockService = MockUserService();
// Установить поведение мока
when(mockService.getUser('123')).thenAnswer(
(_) async => User(id: '123', name: 'John Doe'),
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: UserProfile(
userId: '123',
service: mockService,
),
),
),
);
// Ждём асинхронную операцию
await tester.pumpAndSettle();
// Проверяем, что сервис был вызван
verify(mockService.getUser('123')).called(1);
// Проверяем, что данные отобразились
expect(find.text('John Doe'), findsOneWidget);
});
}
3. golden_toolkit для golden файлов
golden_toolkit — для визуального тестирования (скриншоты).
import 'package:golden_toolkit/golden_toolkit.dart';
void main() {
group('Golden Tests', () {
testGoldens('MyButton looks correct', (WidgetTester tester) async {
await tester.binding.window.physicalSizeTestValue = const Size(400, 800);
addTearDown(tester.binding.window.clearPhysicalSizeTestValue);
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: MyButton(label: 'Click me'),
),
),
);
await expectLater(
find.byType(MyButton),
matchesGoldenFile('goldens/my_button.png'),
);
});
});
}
Примеры widget-тестов
Пример 1: Простой button
class MyButton extends StatefulWidget {
final String label;
final VoidCallback onPressed;
const MyButton({required this.label, required this.onPressed});
@override
State<MyButton> createState() => _MyButtonState();
}
class _MyButtonState extends State<MyButton> {
int clickCount = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: () {
setState(() => clickCount++);
widget.onPressed();
},
child: Text(widget.label),
),
Text('Clicked: $clickCount'),
],
);
}
}
// Тест
void main() {
group('MyButton Tests', () => {
testWidgets('button shows label', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MyButton(
label: 'Press me',
onPressed: () {},
),
),
),
);
expect(find.text('Press me'), findsOneWidget);
});
testWidgets('button increments counter', (tester) async {
var callCount = 0;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MyButton(
label: 'Click',
onPressed: () => callCount++,
),
),
),
);
expect(find.text('Clicked: 0'), findsOneWidget);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(find.text('Clicked: 1'), findsOneWidget);
expect(callCount, 1);
});
});
}
Пример 2: Статефул виджет с async операциями
class UserListView extends StatefulWidget {
final UserService userService;
const UserListView({required this.userService});
@override
State<UserListView> createState() => _UserListViewState();
}
class _UserListViewState extends State<UserListView> {
late Future<List<User>> _usersFuture;
@override
void initState() {
super.initState();
_usersFuture = widget.userService.getUsers();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<User>>(
future: _usersFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return ListView.builder(
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (context, index) {
final user = snapshot.data![index];
return ListTile(title: Text(user.name));
},
);
},
);
}
}
// Тест
void main() {
group('UserListView Tests', () {
testWidgets('shows loading state', (tester) async {
final mockService = MockUserService();
when(mockService.getUsers()).thenAnswer(
(_) => Future.delayed(Duration(seconds: 5), () => []),
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: UserListView(userService: mockService),
),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('displays users after loading', (tester) async {
final users = [
User(id: '1', name: 'Alice'),
User(id: '2', name: 'Bob'),
];
final mockService = MockUserService();
when(mockService.getUsers()).thenAnswer(
(_) async => users,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: UserListView(userService: mockService),
),
),
);
await tester.pumpAndSettle(); // Ждём завершения всех анимаций
expect(find.text('Alice'), findsOneWidget);
expect(find.text('Bob'), findsOneWidget);
});
testWidgets('shows error message on failure', (tester) async {
final mockService = MockUserService();
when(mockService.getUsers()).thenThrow(
Exception('Network error'),
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: UserListView(userService: mockService),
),
),
);
await tester.pumpAndSettle();
expect(
find.byWidgetPredicate(
(widget) => widget is Text && widget.data?.contains('Error') ?? false,
),
findsOneWidget,
);
});
});
}
Методы tester
Основные методы WidgetTester:
// Поиск виджетов
find.byType(MyWidget)
find.byIcon(Icons.add)
find.text('Button Label')
find.byKey(ValueKey('myKey'))
find.byWidget(widget)
// Взаимодействие
await tester.tap(finder) // Нажать
await tester.drag(finder, offset) // Потянуть
await tester.typeText('text') // Вводить текст
// Перестроение
await tester.pump() // Перестроить один раз
await tester.pump(Duration(ms: 300)) // С задержкой
await tester.pumpAndSettle() // Ждать завершения анимаций
// Проверки
expect(finder, findsOneWidget)
expect(finder, findsWidgets)
expect(finder, findsNothing)
expect(value, equals(expected))
Best Practices
1. Используй AAA паттерн (Arrange, Act, Assert)
testWidgets('test example', (tester) async {
// Arrange - подготовка
await tester.pumpWidget(MyApp());
// Act - выполнение
await tester.tap(find.byType(Button));
await tester.pump();
// Assert - проверка
expect(find.text('Updated'), findsOneWidget);
});
2. Мокируй зависимости
final mockService = MockMyService();
when(mockService.getData()).thenAnswer((_) async => []);
3. Не полагайся на timing
// ❌ Плохо
await Future.delayed(Duration(seconds: 1));
// ✅ Хорошо
await tester.pumpAndSettle();
4. Тестируй user interactions
await tester.tap(find.byType(Button));
await tester.typeText(find.byType(TextField), 'text');
await tester.drag(finder, offset);
Запуск тестов
# Все widget-тесты
flutter test test/widget_test.dart
# С coverage
flutter test --coverage
# Watch режим
flutter test --watch
Вывод
Для widget-тестирования я использовал flutter_test для фреймворка, mockito для моков зависимостей, и golden_toolkit для визуальных тестов. Это обеспечило хорошее покрытие тестами и confidence в качестве кода.