Тестирование Flutter-приложений
Три уровня, которые даёт Flutter, чем хорош каждый из них и как писать тесты, которые падают, когда сломан продукт, а не когда переехал widget.
Почему это важно. Ценность теста — это пойманная ошибка минус потраченное на него время. Набор тестов, каждый из которых проверяет текущую реализацию, стоит дорого, а ловит мало; он усложняет рефакторинг, то есть делает ровно обратное тому, ради чего тесты нужны.
Что нужно понимать
- Что этот тест поймает такого, чего не поймает ничто другое
- Проверяет он поведение или реализацию
- Что подменено и насколько подмена всё ещё похожа на реальность
- Почему он медленный, если он медленный
- Понятно ли из упавшего теста, что именно сломалось
Ключевые темы
Уровни
- Unit-тесты: чистая логика, миллисекунды, основная часть набора
- Widget-тесты: один widget или экран с настоящим деревом, без устройства
- Интеграционные тесты: приложение целиком на устройстве или эмуляторе
- Golden-тесты для отрисовки и цена их поддержки
Widget-тесты
pumpWidget,pumpиpumpAndSettle— и что каждый из них делает на самом деле- Finders и почему искать по семантике лучше, чем по типу
- Время: fake async, таймеры и анимации, которые никогда не заканчиваются
- Подмена зависимостей по правильному шву
- Загрузка, ошибка и пустой результат как полноценные случаи, а не как довесок
Как сделать код тестируемым
- Внедрять зависимости, а не тянуться к глобальным объектам
- Держать логику вне widget'ов — тогда большей её части widget-тесты не нужны
- Швы для сети, хранилища, времени и случайности
- Fake вместо mock там, где fake проще
Набор тестов
- Скорость: медленный набор просто перестают запускать
- Нестабильный тест — это ошибка, а не свойство природы
- Покрытие как подсказка, но никогда как цель
- Что гоняется на каждый коммит, а что — ночью
Уровни
| Уровень | Как это выглядит |
|---|---|
| Junior | Пишет unit-тесты для функций и следует принятым в проекте образцам widget-тестов. |
| Middle | Выбирает подходящий уровень, подменяет зависимости по разумным швам, проверяет состояния, а не только удачный сценарий, чинит нестабильные тесты. |
| Senior | Выстраивает быстрый набор тестов, которому можно верить, держит тесты на уровне поведения, чтобы рефакторинг оставался дешёвым, и понимает, что тестировать не надо. |
Практика
Для начала
-
Проверьте состояния Напишите widget-тесты на состояния загрузки, пустого результата и ошибки, а не только на успешное.
-
Уберите один
pumpAndSettleНайдите тот, что зависает на повторяющейся анимации, и замените его явными вызовамиpump. -
Подмените часы Протестируйте что-нибудь зависящее от времени, не прибегая к
sleep.
Дальше
-
Тестируйте поведение, а не структуру Возьмите тест, который ломается на каждом рефакторинге, и перепишите его на проверку поведения.
-
Почините нестабильный тест Найдите тест, который падает через раз, разберитесь, где на самом деле гонка, и устраните причину.
-
Добавьте один интеграционный тест Покройте самый важный для вас сценарий целиком, от начала до конца, и запускайте его в CI.
Проверьте себя
- Какие из ваших тестов пройдут, даже если функциональность сломана?
- Какие падают при каждом рефакторинге, который не меняет поведение?
- Сколько идёт ваш набор тестов и когда это в последний раз кого-нибудь волновало?
- Что подменено в ваших widget-тестах и соответствует ли подмена реальности до сих пор?
- Поломка какого сценария обойдётся дороже всего и проверен ли он целиком, от начала до конца?
- Чего вы сознательно не тестируете и почему?
Материалы
- Testing Flutter apps — официальный обзор трёх уровней и того, когда какой уместен.
- Widget testing introduction —
практическая отправная точка: finders и всё семейство
pump. - flutter_test API —
стоит один раз пролистать: в
WidgetTesterзаметно больше, чем используют туториалы. - Test Desiderata — Кент Бек о двенадцати свойствах хорошего теста. Самый внятный набор критериев, чтобы решить, окупается ли тест.
- mocktail — подмена зависимостей с null safety и без кодогенерации, и документация, которая честно говорит, когда fake лучше mock.