Всем привет, на связи Surf. Ранее мы рассмотрели процесс создания небольшого приложения с использованием пакета Elementary и разобрали, как он устроен. Теперь поговорим о тестировании.
В статье расскажу, какие виды тестов бывают, и на примере покажу, как покрыть тестами приложение, написанное при помощи Elementary.
Пакет доступен на pub.dev. Исходный код можно посмотреть на GitHub.
Сравнение видов тестов
В официальной документации есть три базовых вида тестов: Unit, Widget, Integration.
Unit-тест проверяет одну функцию, метод или класс. Цель — убедиться, что функция выполняется правильно в различных условиях.
Widget-тест проверяет один виджет. Цель — убедиться, что пользовательский интерфейс виджета выглядит и ведёт себя так, как ожидалось. Тестирование виджета включает несколько классов и требует тестовой среды: она обеспечивает соответствующий контекст.
Integration-тест проверяет полное приложение или большую его часть. Цель — убедиться, что все виджеты и сервисы работают вместе, как ожидалось. Кроме того, интеграционные тесты используют для проверки производительности приложения.
Surf использует ещё один тип тестов — Golden. В официальной документации о нём не упоминается, но благодаря пакету golden_toolkit golden-тесты во Flutter стали возможны.
Golden-тест проверяет отдельные виджеты и целый экран. Визуальное представление компонента сравнивается с предыдущими результатами тестов.
![Итоговая таблица сравнения типов тестирования Итоговая таблица сравнения типов тестирования](https://habrastorage.org/getpro/habr/upload_files/94c/27c/b4a/94c27cb4ab2458b4e8c5daea0f946968.png)
Тестируем UI
Покроем тестами готовое приложение, которое писали в рамках статьи «Elementary: новый взгляд на архитектуру Flutter-приложений». Проверять визуальное представление будем при помощи golden-тестов. Их легко освоить и поддерживать — если правильно соблюдать зависимости.
Если вы используете IDEA, добавьте конфигурацию запуска теста для генерации golden.
![](https://habrastorage.org/getpro/habr/upload_files/15e/d4e/087/15ed4e08748429b967c5dab6c4cb8ffd.png)
Также генерацию golden-тестов можно выполнить через CLI при помощи команды.
flutter test -- update-goldens
Перед началом работы с golden-тестами нужно добавить конфигурацию: положить в папку тест flutter_test_config.dart.
Код ниже выполняется при каждом тесте и подгружает шрифты.
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
await loadAppFonts();
return testMain();
}
Когда команда генерации голденов будет выполнена, получим набор изображений: они будут расположены в директории с файлом теста.
![](https://habrastorage.org/getpro/habr/upload_files/6a8/a25/f1c/6a8a25f1cf2620146d8b8ddd747e4893.png)
![](https://habrastorage.org/getpro/habr/upload_files/9ad/e03/9dc/9ade039dcb45a7f7b286e29a478858a3.png)
За счёт мокированного интерфейса можно задавать различные сценарии и получать визуальные представления.
Магия простоты заключается в том, что ещё на этапе написания WM мы создаём интерфейс, который и определяет набор параметров. Далее просто мокируем нужные значения и прогоняем через тест.
Тест экрана выбора города
void main() {
const selectAddressScreen = SelectAddressScreen();
final selectAddressWm = SelectAddressWMMock();
setUp(() {
when(() => selectAddressWm.predictions).thenAnswer(
(_) => ValueNotifier<List<Location>>([]),
);
when(() => selectAddressWm.searchFieldController).thenAnswer(
(_) => TextEditingController(),
);
});
testGoldens('select address screen default golden test', (tester) async {
await tester.pumpWidgetBuilder(selectAddressScreen.build(selectAddressWm));
await multiScreenGolden(tester, 'select_address_screen');
});
testGoldens('select address screen with data golden test', (tester) async {
when(() => selectAddressWm.predictions).thenAnswer(
(_) => ValueNotifier<List<Location>>(_locationMock),
);
await tester.pumpWidgetBuilder(selectAddressScreen.build(selectAddressWm));
await multiScreenGolden(tester, 'select_address_screen_data');
});
}
Тест экрана прогноза погоды
void main() {
final wm = WeatherScreenWMMock();
const weatherScreen = WeatherScreen();
setUp(() {
when(() => wm.topPadding).thenReturn(16);
when(() => wm.currentWeather).thenReturn(
EntityStateNotifier.value(_mockWeathers),
);
when(() => wm.locationTitle).thenReturn(_locationMock.title);
});
testGoldens('weather details screen with data golden test', (tester) async {
await tester.pumpWidgetBuilder(weatherScreen.build(wm));
await multiScreenGolden(tester, 'weather_details_screen_data');
});
testGoldens('weather details screen with error golden test', (tester) async {
when(() => wm.currentWeather).thenReturn(
EntityStateNotifier.value([])..error(Exception()),
);
await tester.pumpWidgetBuilder(weatherScreen.build(wm));
await multiScreenGolden(tester, 'weather_details_screen_err');
});
}
Тестируем WidgetModel
Для тестирования WidgetModel из пакета Elementary необходимо подключить библиотеку elementary_test: она предлагает несколько удобных инструментов для тестирования.
testWidgetModel — функция самого теста. При прохождении WidgetModel она описывает его поведение и проверяет результат. Функция также использует тестер для манипуляции фазами жизненного цикла WidgetModel, а BuildContext — для их имитации.
При написании теста стоит держать в голове, что окружение вокруг тестируемого объекта может быть полностью замокировано.
Тест виджет-модели экрана выбора города
void main() {
group('init select address screen wm', () {
final getIt = GetIt.instance;
setUp(() {
getIt.registerSingleton<AppModel>(AppModel());
});
test('createSelectAddressWM', () {
expect(() => createSelectAddressWM(BuildContextMock()), returnsNormally);
});
});
group('select address screen wm testing', () {
late SelectAddressModelMock modelData;
late NavigationHelperMock navigatorStateMock;
SelectAddressWM setupWm() {
modelData = SelectAddressModelMock();
navigatorStateMock = NavigationHelperMock();
when(() => modelData.getCityPrediction(any()))
.thenAnswer((invocation) => Future.value());
registerFallbackValue(MaterialPageRoute<void>(builder: (_) {
return const Center();
}));
return SelectAddressWM(modelData, navigatorStateMock);
}
testWidgetModel<SelectAddressWM, SelectAddressScreen>(
'onTapLocation call onLocationSelected and navigate to next screen',
setupWm,
(wm, tester, context) async {
tester.init();
wm.onTapLocation(_locationMock);
verify(() => modelData.onLocationSelected(_locationMock));
verify(() => navigatorStateMock.push(context, any()));
},
);
testWidgetModel<SelectAddressWM, SelectAddressScreen>(
'onTextChanged call getCityPrediction',
setupWm,
(wm, tester, context) async {
tester.init();
wm.searchFieldController.text = 'Test';
verify(() => modelData.getCityPrediction(any()));
},
);
});
}
Тест виджет-модели экрана прогноза погоды
void main() {
group('WeatherScreenWm init', () {
final getIt = GetIt.instance;
setUp(() {
getIt.registerSingleton<AppModel>(AppModel());
});
test('createWeatherScreenWM', () {
expect(() => createWeatherScreenWM(BuildContextMock()), returnsNormally);
});
});
group('WeatherScreenWM', () {
final modelData = WeatherScreenModelMock();
final contextHelperMock = ContextHelperMock();
WeatherScreenWM setupWm() {
when(modelData.getWeather).thenAnswer((invocation) => Future.value([]));
return WeatherScreenWM(contextHelperMock, modelData);
}
testWidgetModel<WeatherScreenWM, WeatherScreen>(
'getWeather called after init wm ',
setupWm,
(wm, tester, context) async {
tester.init();
verify(modelData.getWeather);
},
);
testWidgetModel<WeatherScreenWM, WeatherScreen>(
'topPadding getter return padding',
setupWm,
(wm, tester, context) async {
tester.init();
when(
() => contextHelperMock.getMediaQuery(context),
).thenReturn(const MediaQueryData());
expect(wm.topPadding, 16);
},
);
testWidgetModel<WeatherScreenWM, WeatherScreen>(
'onRetryPressed call getWeather',
setupWm,
(wm, tester, context) async {
tester.init();
wm.onRetryPressed();
verify(modelData.getWeather);
},
);
});
}
Тестируем модель
Для тестирования модели используем unit-тесты. Перед написанием желательно ознакомиться с набором готовых матчеров: same, isTrue, isFalse, returnsNormally и так далее.
Тест модели экрана выбора города
void main() {
final addressServiceMock = AddressServiceMock();
late SelectAddressModel model;
setUp(() {
model = SelectAddressModel(addressServiceMock, AppModel());
});
test('init with empty list', () async {
when(() => addressServiceMock.getCityPredictions('')).thenAnswer(
(_) => Future.value([]),
);
expect(model.predictions.value, isEmpty);
});
test('getCityPrediction return empty list', () async {
when(() => addressServiceMock.getCityPredictions('Test')).thenAnswer(
(_) => Future.value(_locationMock),
);
await model.getCityPrediction('');
expect(model.predictions.value, isEmpty);
});
test('getCityPrediction return prediction list', () async {
await model.getCityPrediction('Test');
expect(model.predictions.value, same(_locationMock));
});
}
Тест модели экрана прогноза погоды
void main() {
late WeatherScreenModel wm;
final weatherServiceMock = WeatherServiceMock();
setUp(() {
wm = WeatherScreenModel(weatherServiceMock, _locationMock);
});
test('location getter return selected location', () {
expect(wm.location, same(_locationMock));
});
test('method getWeather return weather from weather service', () async {
when(() => weatherServiceMock.getWeather(any())).thenAnswer(
(invocation) => Future.value(_weatherMock),
);
expect(await wm.getWeather(), same(_weatherMock));
});
}
Проверяем покрытие
Инструкция по проверке покрытия испытания:
Собираем информацию о покрытии тестами.
flutter test --coverage --update-goldens
Генерируем HTML-отчёт.
genhtml coverage/lcov.info -o coverage/html
Открываем сгенерированный отчёт.
open coverage/html
Получаем детальную информацию о состоянии покрытия тестами. Также можно посмотреть места, ещё не покрытые тестами. Это очень удобный инструмент, если вы активно пишете тесты.
![](https://habrastorage.org/getpro/habr/upload_files/f9d/338/d8c/f9d338d8c94a6d227464da2119897b91.png)
Мы на практике увидели, что приложение, написанное при помощи пакета Elementary, можно легко протестировать всеми видами тестов. Это стало возможно благодаря разделению приложения на слои и отсутствию большой связанности между ними.
Комментарии (3)
ZiggiPop
11.02.2022 11:21-1Извините, но у меня из-за вашей КДВП случился парциальный эпилептический приступ.
Neikist
Вы там с дубу рухнули, анимацию как кдпв использовать?
ZiggiPop
Ой, извините, случайно заминусил ваш комментарий :(