Модульное тестирование — это часть автоматизированного процесса тестирования, в котором тестируются небольшие блоки кода.

Всем привет. В этой статье мы узнаем все о модульном тестировании во Flutter и Dart.

Повестка дня

Что такое автоматизированное тестирование?

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

Во-первых, нам нужно кое-что понять. Автоматизированное тестирование — это всего лишь способ предотвратить ожидаемые ошибки в программном обеспечении. Это не значит, что в программе нет ошибок. Это просто означает, что в программном обеспечении нет ожидаемых ошибок.

«Тестирование показывает наличие, а не отсутствие ошибок». Эдсгер Дейкстра.

Цикл всегда выглядит следующим образом:

Как мы видим, после этапа разработки мы пишем ожидаемые тестовые случаи (TDD — исключение). После прохождения всех тестов мы обычно отправляем программное обеспечение. Пока программное обеспечение находится в производстве, если возникает какая-либо ошибка, мы исправляем ее и пишем тестовые примеры, чтобы предотвратить появление такой же ошибки в программном обеспечении в будущем.

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

Что такое модульное тестирование?

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

Модульное тестирование — это самый нижний уровень в наборе тестов. На этом уровне мы проверяем внутреннюю работу каждой функции.

Модульный тест состоит из трех этапов:

  • Договариваться
  • Действовать
  • Утверждать

На этапе аранжировки нам просто нужно создать объект модуля, который нам нужно протестировать, и подготовить предварительные условия для нашего теста, т. е. установить переменную состояния, настроить макеты и т. д. Этап аранжировки может существовать, а может и не существовать. исходя из потребности.

На этапе действия мы запускаем модуль с некоторым состоянием (передавая аргументы) и сохраняем результат, если он есть.

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

Фаза «Упорядочить и действовать» не является обязательной.

Зачем нам модульное тестирование?

  • Модульное тестирование очень легко написать и запустить. Таким образом экономится много времени.
  • Модульное тестирование помогает нам выявлять ошибки на ранней стадии. Таким образом экономится много времени и денег.
  • Поскольку мы пишем все ожидаемые случаи модуля, любой может понять, что это за модуль. Таким образом действует как лучшая документация.
  • Часто мы не рефакторим наш код, думая, что это может сломать модуль. Наличие модульных тестов дает нам уверенность в рефакторинге нашего кода.
  • Отладка проста. Поскольку мы точно знаем случаи, которые дают сбой, мы можем точно определить модуль, вызывающий ошибку.
  • Глядя на тестовые случаи, мы можем легко понять, что представляет собой модуль. Таким образом, долгосрочное обслуживание проще.

Что мы можем протестировать в модуле?

Чтобы иметь хороший набор модульных тестов, нам нужно понимать, что нам нужно тестировать в модуле.

Обычно модульное тестирование основано на следующих аспектах:

  • Переменные состояния
  • Вызовы функций/переменных
  • Аргументы функции
  • Функция возвращает

Переменные состояния — это переменные вне локальной области видимости. Это может быть глобальная переменная или свойство класса, к которому могут обращаться более чем одно устройство. Обычно он содержит состояние.

Вот некоторые из случаев, которые нам нужно искать в блоке:

  • Проверьте значение константы или конечной переменной.
  • Начальные значения переменных состояния.
  • Проверьте, вызывает ли устройство определенную функцию 1…n раз.
  • Убедитесь, что устройство никогда не вызывает определенную функцию.
  • Убедитесь, что переменные состояния обновлены должным образом.
  • Результат блока такой же, как и ожидалось.
  • Если задействована строка, список или любой другой сложный DS, обязательно проверяйте пустые случаи, особенно если мы проходим через DS.
  • Проверка нулевых случаев (только для типов, допускающих значение NULL. Теперь Dart безопасен для NULL)
  • Проверьте тип переменной или аргумента (не нужно, если мы хорошо используем систему типов Dart)

Нулевая безопасность и система типов Dart экономят много тестов во всех сценариях.

Как выполнить модульное тестирование в Dart?

  • Во-первых, нам нужно включить зависимость в файл pubspec.yaml.
  • Создайте тестовый файл дротика с суффиксом _test. Это помогает анализатору dart рассматривать файл как тестовый файл.

Мы собираемся проверить площадь круга.

sample_project/
  lib/
    area.dart
    main.dart
  test/
    area_test.dart

Вот тестовый файл Dart. В каждом тестовом файле должен быть метод main(), в котором начинается выполнение.

Нам нужно обернуть наш тестовый пример внутри функции и передать функцию методу test() в качестве аргумента вместе с надлежащим описанием тестового примера.

Этот тест предполагает, что площадь круга будет равна 3,141592, когда радиус равен 1. Здесь единицей измерения является метод circle().

  • На этапе аранжировки (настройки) мы создали экземпляр класса Area.
  • На этапе действия (запуска) мы запускали модуль, который хотели протестировать, с некоторыми входными данными.
  • На этапе подтверждения (проверки) мы сравнили результат с ожидаемым результатом.

Вот еще один тест, который просто проверяет значение геттера pi:

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

Группировка тестовых случаев

Часто нам нужно написать много похожих тестовых случаев. Метод group() помогает нам группировать похожие тестовые случаи для удобства управления.

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

Метод setUpApp() запускается первым перед всеми тестами. Используя это, мы можем уменьшить повторение.

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

Если объект содержит состояние, то наличие общего экземпляра для всех состояний может привести к возникновению многих проблем. Таким образом, нам нужны отдельные экземпляры для каждого теста.

Недопустимо иметь общее состояние для многих тестовых наборов.

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

Метод setUp() похож на метод setUpAll(), но разница в том, что он запускается каждый раз перед тестовым набором в текущей области. Таким образом, каждый тестовый пример получает свое собственное состояние.

Здесь метод setUp() запускается перед каждым тестовым случаем. Таким образом, имея отдельные экземпляры для каждого теста. Таким образом, никаких государственных проблем.

Всегда предпочитайте метод setUp() методу setUpAll(), если обратный вызов не работает медленно.

Метод tearDownAll() можно использовать для запуска чего-либо после завершения всех тестов в текущей области. Точно так же метод the tearDown() используется для запуска чего-либо после каждого теста. Обычно используется для удаления экземпляров или отмены подписки.

setUp(), setUpAll(), tearDown() и tearDownAll() можно использовать внутри group() для ограничения области действия.

Надлежащая практика модульного тестирования

  • Модульные тесты должны быть быстрыми
  • Модульные тесты должны быть простыми
  • Модульные тесты должны быть детерминированными
  • Модульные тесты должны быть сфокусированы
  • Повторение кода допустимо в модульных тестах
  • Описание теста должно быть понятным

Модульные тесты должны быть быстрыми. Весь наш набор модульных тестов должен запускаться в течение пары минут, поэтому мы сами запустим набор тестов на этапе разработки. Это помогает своевременно исправлять ошибки. Если это занимает больше времени, мы обычно бросаем эту работу в какой-то конвейер.

Модульные тесты должны быть простыми. Когда мы проходим модульный тестовый пример, все, что нам нужно знать, должно быть внутри этого тестового примера. Мы не хотим прокручивать код, чтобы понять один тестовый пример. Юнит-тест должен говорить сам за себя.

Модульные тесты должны быть детерминированными. Модульный тест должен вести себя точно так же, как и раньше, где бы и когда бы он ни тестировался, без изменения исходного кода. Модульный тест не должен зависеть от каких-либо внешних факторов, таких как Current-Time, база данных, сеть, собственный API и т. д. Обычно мы издеваемся над ними.

Модульные тесты должны быть сфокусированы. Модульный тест должен фокусироваться только на одном модуле. Мы не должны тестировать зависимости внутри модульного теста.

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

Описание юнит-теста должно быть понятным. Хорошее описание должно состоять из четырех частей:

  • Устройство, которое мы собираемся протестировать
  • Текущее состояние объекта
  • Вход, который мы собираемся дать
  • Ответ, который мы ожидаем

В этом описании теста «площадь круга с радиусом 1 должна быть 3,141592», мы можем разбить его на части следующим образом:

  • «Площадь круга» — это единица измерения, которую мы будем тестировать.
  • «Радиус 1 — это ввод, который мы собираемся использовать.
  • «3.141592» — это ожидаемый результат.

Эта единица не имеет никакого состояния.

Насмешка

Основная идея модульного тестирования состоит в том, чтобы изолировать и сосредоточиться на текущем модуле, который мы тестируем, а не на поведении внешних зависимостей. Но в большинстве случаев нам нужно зависеть от внешних зависимостей, таких как БД, веб-серверы, API платформы, внешние устройства и т. д.

Предположим, что наш текущий модуль зависит от веб-API. Тест работает медленно, но нормально, когда сервер работает. Но когда сервер отключен, модульный тест не выполняется. Это делает модульный тест непредсказуемым. Потому что веб-сервер не будет под нашим контролем. Это не наша вина, когда веб-сервер выходит из строя. Вот тут и начинается насмешка.

Имитация — это просто процесс, используемый в модульном тестировании, когда тестируемый модуль имеет внешние зависимости. Цель имитации — изолировать тестируемый код и сосредоточиться на нем, а не на поведении внешних зависимостей.

Давайте посмотрим пример.

Мы собираемся протестировать этот метод fetch() в классе репозитория. Как мы видим, наш модуль зависит от метода RemoteDataSource’s fetch(), который взаимодействует с веб-API, который является внешней зависимостью. Итак, нам нужно заглушить метод fetch() из RemoteDataSource.

Заглушка – это изменение поведения какой-либо части устройства, не влияющее ни на что другое.

Есть несколько способов, которыми мы могли бы поиздеваться.

Реализация класса, который нам нужно издеваться

Здесь мы реализовали класс RemoteDateSource в двух фиктивных классах. MockRemoteDateSourceSuccess, который имитирует случай успеха, и MockRemoteDateSourceFailure, который имитирует случай отказа.

При написании тестовых случаев мы можем выбрать, какая реализация нам нужна на основе теста.

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

Расширение абстрактного класса, который нам нужно имитировать

Это похоже на предыдущий метод. Единственное отличие состоит в том, что вместо реализации конкретного класса мы просто расширяем класс abstract, который расширяет конкретный класс.

Это помогает сократить количество ненужных реализаций конкретного класса.

Но у обоих этих методов есть две проблемы.

  • Им не хватает простоты, поскольку нам нужно перейти к фиктивному коду, чтобы узнать о деталях его реализации.
  • Кроме того, нам нужно иметь отдельные фиктивные классы для всех тестовых случаев. Некоторая часть повторения кода приемлема для модульных тестов. Но такое количество повторений кода трудно поддерживать.

Здесь в игру вступают фиктивные пакеты.

Mockito — мок-библиотека для Dart

Этот пакет решает обе эти проблемы. Идея библиотеки Mockito заключается в том, что нам просто нужно использовать аннотацию @GenerateMocks() для передачи классов, которые нам нужно имитировать. Он генерирует для нас мок-класс с помощью пакета build_runner (не забудьте включить build_runner как dev_dependency).

Здесь мы сделали заглушку, используя метод when() из Mockito.

Не забудьте сгенерировать mock-классы.

dart run build_runner build --delete-conflicting-outputs

build_runner создаст файл с именем на основе файла, содержащего аннотацию @GenerateMocks. Здесь, в примере repository_test.dart, мы импортируем сгенерированную библиотеку как repository_test.mocks.dart.

  • Нам не нужно никуда двигаться, чтобы увидеть логику реализации. Все было бы прямо там.
  • Создание фиктивного класса выполняется mockito, поэтому его легко поддерживать.

Мы можем просто сосредоточиться на заглушке, действовать и утверждать. Вот и все.

Единственная проблема с Mockito — генерация кода. Мы можем преодолеть это с помощью MockTail.

MockTail — без генерации кода

Mocktail фокусируется на предоставлении знакомого простого API для создания макетов в Dart (с нулевой безопасностью) без необходимости создания макетов вручную или генерации кода.

Эта библиотека написана Феликсом Ангеловым, тем же человеком, который написал bloc, equatable и т. д. Эта библиотека даже имеет 100% покрытие кода, как и другие пакеты, которые он написал.

Вот и все. Никаких аннотаций и генерации кода. Всего одна строка.

class MockRemoteDataSource extends Mock implements RemoteDateSource{}

Молодцы, народ! Вы готовы к модульному тестированию.

Надеюсь, вам понравилась эта статья.

Want to Connect?
Connect with me on LinkedIn, Twitter, GitHub.