← Назад к вопросам

Что использовал для написания widget-test?

1.2 Junior🔥 161 комментариев
#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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 в качестве кода.