Рубрики
Flutter

Тестирование во Flutter: Руководство 2025

Полное руководство по тестированию Flutter приложений в 2025: unit, widget, integration тесты с mocking и best practices.

Тестирование — критическая часть разработки 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 — это обязательная практика. Используйте все три типа тестов для надёжных приложений.