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

Как тестировать Flutter-приложения? Какие виды тестов существуют?

2.0 Middle🔥 252 комментариев
#Тестирование

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

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

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

Тестирование во Flutter

Три типа тестов

Фреймворк Flutter поддерживает три основных вида тестирования:

┌─────────────────────────────────────────────┐
│           Unit Tests (Юнит-тесты)          │
│  Тестирование отдельных функций, классов  │
│  Быстро | Не нужен эмулятор | Просто      │
└─────────────────────────────────────────────┘
       ↓
┌─────────────────────────────────────────────┐
│         Widget Tests (Виджет-тесты)        │
│  Тестирование UI компонентов и виджетов   │
│  Средняя скорость | Изолированные тесты   │
└─────────────────────────────────────────────┘
       ↓
┌─────────────────────────────────────────────┐
│   Integration Tests (E2E тесты)            │
│  Полное приложение от начала до конца     │
│  Медленно | Нужен эмулятор | Реальные BL  │
└─────────────────────────────────────────────┘

1. Unit Tests (Юнит-тесты)

Тестируем функции, классы, логику БЕЗ UI:

import "package:flutter_test/flutter_test.dart";

void main() {
  group("Calculator Tests", () {
    // Простая функция для тестирования
    int add(int a, int b) => a + b;
    int subtract(int a, int b) => a - b;

    test("add returns correct sum", () {
      expect(add(2, 3), equals(5));
      expect(add(-1, 1), equals(0));
      expect(add(0, 0), equals(0));
    });

    test("subtract returns correct difference", () {
      expect(subtract(5, 3), equals(2));
      expect(subtract(-1, -1), equals(0));
    });

    test("divide by zero throws exception", () {
      expect(
        () => 10 ~/ 0,
        throwsException,
      );
    });
  });
}

2. Widget Tests (Виджет-тесты)

Тестируем отдельные виджеты в изоляции:

import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";

void main() {
  group("Counter Button Tests", () {
    testWidgets("Button increments counter", (WidgetTester tester) async {
      // Собрать виджет
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: Counter(),
          ),
        ),
      );

      // Проверить начальное состояние
      expect(find.text("0"), findsOneWidget);
      expect(find.byIcon(Icons.add), findsOneWidget);

      // Нажать на кнопку
      await tester.tap(find.byIcon(Icons.add));
      await tester.pump(); // Пересчитать виджет

      // Проверить новое состояние
      expect(find.text("1"), findsOneWidget);
    });

    testWidgets("Multiple taps work correctly", (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(body: Counter()),
        ),
      );

      // Тапнуть 5 раз
      for (int i = 0; i < 5; i++) {
        await tester.tap(find.byIcon(Icons.add));
        await tester.pump();
      }

      expect(find.text("5"), findsOneWidget);
    });

    testWidgets("Text field accepts input", (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: TextField(),
          ),
        ),
      );

      await tester.enterText(find.byType(TextField), "Hello");
      await tester.pump();

      expect(find.text("Hello"), findsOneWidget);
    });
  });
}

class Counter extends StatefulWidget {
  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text("$count"),
        IconButton(
          icon: Icon(Icons.add),
          onPressed: () => setState(() => count++),
        ),
      ],
    );
  }
}

3. Integration Tests (E2E тесты)

Тестируем полное приложение:

// integration_test/app_test.dart

import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "package:my_app/main.dart";

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group("App Integration Tests", () {
    testWidgets("Full user flow", (WidgetTester tester) async {
      // Запустить приложение
      app();
      await tester.pumpAndSettle();

      // Нажать на кнопку "Sign Up"
      await tester.tap(find.text("Sign Up"));
      await tester.pumpAndSettle();

      // Ввести email
      await tester.enterText(
        find.byType(TextField).first,
        "test@example.com",
      );

      // Ввести пароль
      await tester.enterText(
        find.byType(TextField).last,
        "password123",
      );

      // Нажать "Create Account"
      await tester.tap(find.text("Create Account"));
      await tester.pumpAndSettle(Duration(seconds: 2));

      // Проверить, что мы на главной странице
      expect(find.text("Welcome"), findsOneWidget);
    });
  });
}

Тестирование с Mockito

import "package:mockito/mockito.dart";

// Создать Mock класс
class MockHttpClient extends Mock implements http.Client {}

void main() {
  group("User Repository Tests", () {
    late MockHttpClient mockHttpClient;
    late UserRepository repository;

    setUp(() {
      mockHttpClient = MockHttpClient();
      repository = UserRepository(mockHttpClient);
    });

    test("fetchUser returns user when API call succeeds", () async {
      // Настроить Mock
      when(mockHttpClient.get(any)).thenAnswer(
        (_) async => http.Response(
          jsonEncode({"id": 1, "name": "John"}),
          200,
        ),
      );

      // Выполнить
      final user = await repository.fetchUser(1);

      // Проверить
      expect(user.name, "John");
      verify(mockHttpClient.get(any)).called(1);
    });

    test("fetchUser throws exception on API error", () async {
      when(mockHttpClient.get(any)).thenAnswer(
        (_) async => http.Response("Not Found", 404),
      );

      expect(
        () => repository.fetchUser(999),
        throwsException,
      );
    });
  });
}

Тестирование BLoC

import "package:bloc_test/bloc_test.dart";

void main() {
  group("CounterBloc Tests", () {
    late CounterBloc counterBloc;

    setUp(() {
      counterBloc = CounterBloc();
    });

    tearDown(() {
      counterBloc.close();
    });

    test("initial state is zero", () {
      expect(counterBloc.state, 0);
    });

    blocTest<CounterBloc, int>(
      "emits [1, 2, 3] when IncrementEvent is added three times",
      build: () => counterBloc,
      act: (bloc) {
        bloc.add(IncrementEvent());
        bloc.add(IncrementEvent());
        bloc.add(IncrementEvent());
      },
      expect: () => [1, 2, 3],
    );
  });
}

Покрытие кода (Code Coverage)

# Запустить тесты с покрытием
flutter test --coverage

# Сгенерировать отчёт
genhtml coverage/lcov.info -o coverage/html

# Открыть отчёт в браузере
open coverage/html/index.html

Команды для запуска тестов

# Unit тесты
flutter test test/unit/

# Widget тесты
flutter test test/widget/

# Интеграционные тесты
flutter test integration_test/

# Все тесты
flutter test

# Watch mode (перезапуск при изменениях)
flutter test --watch

# Конкретный файл
flutter test test/counter_test.dart

# Фильтр по названию
flutter test --name "Counter"

Best Practices

  • Юнит тесты: тестируйте логику, бизнес-слой, утилиты
  • Виджет тесты: тестируйте отдельные компоненты UI
  • Интеграционные тесты: критические пути пользователя
  • Покрытие: стремитесь к 80%+ покрытию
  • Читаемость: используйте описательные имена тестов
  • Изоляция: каждый тест должен быть независимым
  • Скорость: юнит тесты должны быть быстрыми

Итог

Пирамида тестирования:

  • Много юнит-тестов (быстро, дёшево)
  • Среднее количество виджет-тестов (баланс)
  • Мало интеграционных тестов (медленные, дорогие)