Всем привет, на связи Surf. Ранее мы рассмотрели процесс создания небольшого приложения с использованием пакета Elementary и разобрали, как он устроен. Теперь поговорим о тестировании.
В статье расскажу, какие виды тестов бывают, и на примере покажу, как покрыть тестами приложение, написанное при помощи Elementary.
Пакет доступен на pub.dev. Исходный код можно посмотреть на GitHub.
Сравнение видов тестов
В официальной документации есть три базовых вида тестов: Unit, Widget, Integration.
Unit-тест проверяет одну функцию, метод или класс. Цель — убедиться, что функция выполняется правильно в различных условиях.
Widget-тест проверяет один виджет. Цель — убедиться, что пользовательский интерфейс виджета выглядит и ведёт себя так, как ожидалось. Тестирование виджета включает несколько классов и требует тестовой среды: она обеспечивает соответствующий контекст.
Integration-тест проверяет полное приложение или большую его часть. Цель — убедиться, что все виджеты и сервисы работают вместе, как ожидалось. Кроме того, интеграционные тесты используют для проверки производительности приложения.
Surf использует ещё один тип тестов — Golden. В официальной документации о нём не упоминается, но благодаря пакету golden_toolkit golden-тесты во Flutter стали возможны.
Golden-тест проверяет отдельные виджеты и целый экран. Визуальное представление компонента сравнивается с предыдущими результатами тестов.
Тестируем UI
Покроем тестами готовое приложение, которое писали в рамках статьи «Elementary: новый взгляд на архитектуру Flutter-приложений». Проверять визуальное представление будем при помощи golden-тестов. Их легко освоить и поддерживать — если правильно соблюдать зависимости.
Если вы используете IDEA, добавьте конфигурацию запуска теста для генерации golden.
Также генерацию golden-тестов можно выполнить через CLI при помощи команды.
flutter test -- update-goldens
Перед началом работы с golden-тестами нужно добавить конфигурацию: положить в папку тест flutter_test_config.dart.
Код ниже выполняется при каждом тесте и подгружает шрифты.
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
await loadAppFonts();
return testMain();
}
Когда команда генерации голденов будет выполнена, получим набор изображений: они будут расположены в директории с файлом теста.
За счёт мокированного интерфейса можно задавать различные сценарии и получать визуальные представления.
Магия простоты заключается в том, что ещё на этапе написания 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
Получаем детальную информацию о состоянии покрытия тестами. Также можно посмотреть места, ещё не покрытые тестами. Это очень удобный инструмент, если вы активно пишете тесты.
Мы на практике увидели, что приложение, написанное при помощи пакета Elementary, можно легко протестировать всеми видами тестов. Это стало возможно благодаря разделению приложения на слои и отсутствию большой связанности между ними.
Комментарии (3)
ZiggiPop
11.02.2022 11:21-1Извините, но у меня из-за вашей КДВП случился парциальный эпилептический приступ.
Neikist
Вы там с дубу рухнули, анимацию как кдпв использовать?
ZiggiPop
Ой, извините, случайно заминусил ваш комментарий :(