Полный гайд по тестированию на Flutter. Часть 3: Mocking и Stubbing
Hola, Amigos! На связи Павел Гершевич, Mobile Team Lead агентства продуктовой разработки Amiga. В предыдущих статьях мы научились писать модульные тесты для статичных функций, верхнеуровневых функций и расширений. Сегодня перевод статьи посвящен Unit-тестам для методов класса.
Больше про кроссплатформенную разработку в телеграмм-канале Flutter.Много. Мы с командой мобильных разработчиков Amiga рассказываем о личном опыте, делимся полезными плагинами\библиотеками, переводами статей и кейсами. Присоединяйтесь!
Написание Unit-тестов для методов класса
Будем использовать пример из прошлых частей, но вместо функции создадим класс LoginViewModel.
Проверим всего 2 тест кейса, например:
В данный момент нет никаких отличий от прошлых частей. Теперь добавим объект SharedPreferences в LoginViewModel и обновим логику функции login.
Как можно заметить, вывод функции login зависит от вывода функции sharedPreferences.getString(email). Поэтому в зависимости от возвращенного результата функции sharedPreferences.getString(email), будут следующие тест кейсы:
- Функция sharedPreferences.getString(email) возвращает storedPassword, который отличается от password, переданного в функцию login.
- Функция sharedPreferences.getString(email) возвращает storedPassword, который совпадает с password, переданным в функцию login.
Для контроля результата функции sharedPreferences.getString(email) необходимо использовать Mocking и Stubbing.
Mocking и Stubbing
Mocking — создание фейкового объекта, который заменяет реальный объект. Mock-объекты часто используются для подмены зависимостей объекта, который нужно протестировать.Кроме того, можно контролировать результат, который возвращают методы Mock-объекта. Эта техника называется Stubbing (заглушки). Например, подменим объект ApiClient и поставим заглушку на его методы get, post, put и delete, чтобы они возвращали фейковые данные вместо выполнения реальных запросов.
В нашем примере нужно подменить объект SharedPreferences, чтобы избежать вызова функций clear или getString в реальности. И что важно — это поможет симулировать результат выполнения функции getString. Таким образом, будет несколько тестовых сценариев для функции login.
Для начала, добавим пакет mocktail в dev_dependencies.
Далее создадим класс с названием MockSharedPreferences, который расширяет класс Mock и реализует класс SharedPreferences.
Теперь создадим Mock-объект внутри функции main.
После этого имитируем mockSharedPreferences, чтобы он возвращал фейковый пароль 123456, используя технику stubbing.
Наконец, протестируем случай, когда пользователь вводит неверный пароль, при помощи имитирования функции sharedPreferences.getString(email). Она возвращает storedPassword, который отличается от password, переданного в функцию login.
Аналогичным образом мы можем проверить и случай, когда пользователь вводит правильный пароль.
Полный исходный код можно найти по ссылке.
Mocktail предлагает 3 способа выполнить stubbing:
– when(() => functionCall()).thenReturn(T expected) используется, когда functionCall — это не асинхронная функция, как в примере выше.
– when(() => functionCall()).thenAnswer(Answer<T> answer) используется, когда functionCall — это асинхронная функция. Например, для подмены функции clear, нужно сделать следующее:
– when(() => functionCall()).thenThrow(Object throwable) используется, когда нужно, чтобы functionCall бросило исключение. Например:
Теперь используем подменные методы для проверки функции logout в 3 тестовых сценариях.
Небольшие изменения в коде, представленном выше:
– Когда ожидаем, что функция выкинет ошибку вместо результата, то не можем вызывать метод logout на шаге Act. Его вызов породит некоторые ошибки, которые перенесутся в функцию тестирования, и это вызовет провал теста. Можем только создать переменную с функцией:
– Когда ожидаем, что функция выкинет ошибку вместо результата, можем использовать доступные для этого Matcher’ы: throwsArgumentError, throwsException и т.д. На примере выше ожидаем, что будет выброшена ошибка FlutterError, поэтому используем expect(call, throwsFlutterError).
– Когда нужно подтвердить более конкретно и подробно. Например, ожидания появления ошибки должно быть FlutterError и его message должен быть “Logout failed”. Тогда нужно использовать 2 Matcher’а: throwsA и isA.
– Matcher throwsA() позволяет проверить выбрасывается ли какая-либо ошибка, включая кастомные классы исключений. На самом деле, throwsFlutterError — это эквивалент throwsA(isA FlutterError()).
– Matcher isA() позволяет проверить тип результата без привязки к определенному значению. Например, когда хотим, чтобы тест вернул либо true, либо false, так как это тип bool, можно использовать expect(result, isA()). Он часто используется с методом having для проведения более детальных проверок за пределами простого типа данных. Например, isA().having((e) => e.message, 'description: error message', 'Logout failed') — тоже самое, что требовать объект быть типа FlutterError и его свойства message равняться 'Logout failed'.
Заключение
В данной статье мы изучили техники Mocking и Stubbing вместе с несколькими часто встречающимися функциями: throwsA, isA и having. В следующей части мы еще больше усложним класс LoginViewModel при помощи создания переменной _cache для кеширования результата, полученного от SharedPreferences. При вызове функции login, мы ставим высший приоритет получению данных из кеша.
Пишите в комментариях, интересна ли вам данная тема? И подписывайтесь на Flutter. Много!