Тестирование вашего приложения Flutter

Тестирование вашего приложения Flutter

Привет всем, сегодня мы поговорим о тестировании с помощью flutter и о важности этого в ваших проектах, так что давайте начнем.

Зачем нам нужно тестирование?

По мере роста нашего проекта flutter растет и важность его тестирования, поэтому, поскольку ваш код был хорошо сделан, используя хорошие паттерны и следуя лучшим практикам, проще внедрить тест, чтобы убедиться, что приложение работает так, как вы задумали.

Концепция

Существует несколько концепций того, как писать тесты и создавать лучшие и хорошо спроектированные приложения, одна из самых распространенных - TDD (Test Driven Development), которая гласит, что вы должны сначала написать тест, а затем написать код, и повторить процесс столько раз, сколько вам нужно, чтобы убедиться, что все проходит тест и код делает только то, что должен делать.

Типы тестов

Во Flutter у нас есть три типа тестов, это:

Юнит-тест: как видно из названия, этот вид тестов предназначен для проверки того, работает ли минимальная часть вашего кода так, как ожидается, сюда входят, например, функции и классы.

Тест виджетов: Этот тест проверяет, работают ли виджеты так, как ожидается, поэтому целью этого вида теста является проверка согласованности пользовательского интерфейса.

Интеграционный тест: Последний, интеграционный тест, лично мне больше всего нравится, он представляет собой смесь двух типов тестов, описанных ранее. С его помощью вы можете проверить весь поток, например, проверить, работает ли оформление заказа в приложении для электронной коммерции так, как ожидалось.

Учитывая все вышесказанное, давайте рассмотрим несколько примеров, чтобы лучше понять суть. В данном случае я покажу, как проверить вход в систему, потому что это общая функция для большинства проектов.

Чтобы начать, предположим, что у вас уже есть flutter в вашем O.S. Тогда просто создайте проект командой flutter create login_test, где имя login_test может быть изменено, если вы хотите дать другое имя проекту, с этим созданным проектом проверьте в корневом каталоге и вы сможете найти папку под названием test, весь ваш код тестирования должен быть в ней, и по умолчанию все классы, созданные для этой цели, должны быть в _test.dart, чтобы язык распознал это как класс тестирования.

Чтобы начать использовать концепцию TDD, давайте сначала создадим модульный тест, а затем реализацию.

Юнит-тест

Удалите файл, поставляемый с проектом, и создайте файл unit_login_test.dart в папке test со следующим кодом:

import 'package:flutter_test/flutter_test.dart';

void main() {
  group('Login controller', () {
    test("success login", () {
      LoginController controller = LoginController();
      expect(controller.login("username", "password"), true);
    });

    test("failed login", () {
      LoginController controller = LoginController();
      expect(controller.login("invalidUsername", "invalidPassword"), false);
    });
  });
}

Мы создаем главную функцию, которая запускает в данном случае эти тесты, мы должны импортировать класс flutter_test для выполнения, как мы хотим, для этого мы создаем группу и внутри помещаем все наши тестовые случаи, инициируем наш класс и выполняем функцию с некоторыми заданными параметрами и ожидаемым возвратом в функции expect test для этого мы представляем, что у нас есть контроллер и внутри функция login, которая получает имя пользователя и пароль и, если они совпадают, возвращает булево значение. Для выполнения теперь мы должны создать файл login_controller.dart в папке lib со следующим кодом:

class LoginController {
  bool login(String username, String password) {
    return username == "username" && password == "password";
  }
}

Таким образом, нам остается только импортировать этот класс в наш тестовый класс и выполнить его, чтобы увидеть результат

import 'package:login_test/login_controller.dart';
Прохождение модульного теста

Теперь перейдем ко второму типу тестов.

Тест виджета

Как и в случае с юнит-тестом, мы создадим тестовый файл и реализуем его, для начала создайте файл widget_login_test.dart и поместите в него следующий код:

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

void main() {
  testWidgets("Login UI", (tester) async {
    await tester.pumpWidget(const MaterialApp(home: LoginPage()));

    final username = find.byKey(const Key("usernameTextField"));
    final password = find.byKey(const Key("passwordTextField"));
    final loginButton = find.byKey(const Key("loginButton"));

    expect(username, findsOneWidget);
    expect(password, findsOneWidget);
    expect(loginButton, findsOneWidget);

    await tester.enterText(username, "username");
    await tester.enterText(password, "password");
    await tester.tap(loginButton);

    await tester.pump();
    final successMessage = find.byKey(const Key('success'));
    expect(successMessage, findsOneWidget);
  });
}

Тест виджетов отображает виджеты, и вы можете выполнять такие действия, как tap() и enterText(), чтобы взаимодействовать с компонентами. В данном тестовом случае нам нужно выполнить поиск с помощью параметра Key, вы можете сделать это с помощью текста, но Key дает вам больше уверенности в том, что вы получите то, что нужно. Теперь для реализации кода просто создайте файл под названием login_page.dart и поместите в него этот код:

import 'package:flutter/material.dart';
import 'package:login_test/login_controller.dart';

LoginController loginController = LoginController();

TextEditingController usernameController = TextEditingController();
TextEditingController passwordController = TextEditingController();

class LoginPage extends StatelessWidget {
  const LoginPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          TextField(
            controller: usernameController,
            key: const Key('usernameTextField'),
          ),
          TextField(
            controller: passwordController,
            key: const Key('passwordTextField'),
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            key: const Key('loginButton'),
            child: const Text("Login"),
            onPressed: () {
              // Example login logic
              if (loginController.login(
                  usernameController.text, passwordController.text)) {
                showDialog(
                  context: context,
                  builder: (_) => const AlertDialog(
                    key: Key('success'),
                    title: Text("Login Successful"),
                  ),
                );
              }
            },
          ),
        ],
      ),
    );
  }
}

Теперь нам нужно только импортировать LoginPage в наш тестовый файл виджета, просто введите import ‘package:login_test/login_page.dart’; с этим вы можете выполнить ваш тест и должны увидеть что-то вроде

Тест виджета

Интеграционный тест

Это последний тип тестирования, он работает путем выполнения полной функции, для этого вам нужно реальное устройство, для начала нам нужно добавить новую зависимость в наш файл pubspec.yaml в разделе dev_dependencies под названием integration_test: sdk: flutter, после этого просто введите flutter pub get для обновления зависимостей, с этой установкой вам нужно создать папку 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:login_test/login_page.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  WidgetsFlutterBinding.ensureInitialized();
  group('LoginPage', () {
    testWidgets("Login UI", (tester) async {
      await tester.pumpWidget(const MaterialApp(home: LoginPage()));

      final username = find.byKey(const Key("usernameTextField"));
      final password = find.byKey(const Key("passwordTextField"));
      final loginButton = find.byKey(const Key("loginButton"));

      expect(username, findsOneWidget);
      expect(password, findsOneWidget);
      expect(loginButton, findsOneWidget);

      await tester.enterText(username, "username");
      await tester.pumpAndSettle(const Duration(seconds: 1));
      await tester.enterText(password, "password");
      await tester.pumpAndSettle(const Duration(seconds: 1));
      await tester.tap(loginButton);

      await tester.pumpAndSettle(const Duration(seconds: 1));
      final successMessage = find.byKey(const Key('success'));
      expect(successMessage, findsOneWidget);
    });
  });
}

этот код очень похож на тест виджетов, но у нас есть несколько отличий, мы используем IntegrationTestWidgetsFlutterBinding.ensureInitialized(); и WidgetsFlutterBinding.ensureInitialized(); чтобы убедиться, что у нас есть интеграционный тест и виджеты, готовые к началу процесса.

Бонус

В дополнительной части этой статьи мы рассмотрим, как получить визуальный отчет о покрытии тестов, если вы не хотите изучать это, просто перейдите к заключению.

Для этого нам нужно запустить в терминале flutter test —coverage, после выполнения которого должна быть создана папка coverage с файлом lcov.info внутри, как мы видим ниже:

Папка с покрытиями

после этого вам нужно преобразовать этот файл в кучу файлов для чтения веб-сервером, для продолжения просто введите в терминале genhtml coverage/lcov.info -o coverage/html, где coverage/lcov.info - это путь к файлу, созданному выше, а coverage/html после флага -o - это каталог, куда я хочу сохранить сгенерированные файлы, когда эта часть закончится, вы увидите папку с заданным именем, созданную там, где вы выбрали.

В завершение нам нужно открыть эту директорию для доступа в веб-браузере, для этого я рекомендую получить библиотеку dhttpd отсюда, просто введите в терминале dart pub global activate dhttpd, этим вы загрузите библиотеку глобально, после этого просто введите dhttpd —path coverage/html/ и откройте браузер, чтобы поместить URL http://localhost:8080 и, наконец, вы сможете увидеть полный отчет о вашем тесте, как показано ниже:

Покрытие флаттера

На этом мы заканчиваем бонусную часть, которая может быть очень важна для понимания того, где код не покрыт и где тесты не работают.

Заключение

Как мы видим, тесты дают нам больше уверенности в конечном результате, сфокусированном на релизе, этот процесс может занять больше времени для завершения функции, но выигрыш от этого более ценен, чем если бы об ошибке сообщили после распространения проекта, так что, надеюсь, все поняли, как делать и важность, надеюсь, увидимся в следующий раз.

Если вы хотите увидеть полный код, я выложил код, ссылка: Репозиторий