[Перевод] Полный гайд по тестированию на Flutter. Часть 5: Mocktail
Hola, Amigos! На связи Павел Гершевич, Mobile Team Lead агентства продуктовой разработки Amiga. В предыдущих статьях, мы разобрали, как использовать библиотеку Mocktail для техник mocking и stubbing в Unit-тестах. Сегодня погрузимся глубже в ее изучение. Оригинал перевода по ссылке. Все выпуски на моей странице. Поехали!
Чтобы не пропускать новые выпуски, подписывайтесь на наш авторский телеграм-канал Flutter. Много. Там мы делимся кейсами, личным опытом, полезными плагинами и библиотеками.
Функции «verify»
Unit-тесты часто проверяют результат выполнения функции, основанный на входных данных. А что делать, если функция не возвращает результат (void методы)? Как тогда их протестировать?
Mocktail предоставляет функцию verify
для проверки, был ли вызван метод и сколько раз это произошло.
Предположим, есть функция login
, которая после успешной авторизации перенаправляет пользователя на домашний экран.
class LoginViewModel {
final Navigator navigator;
LoginViewModel({
required this.navigator,
});
void login(String email) {
if (email.isNotEmpty) {
navigator.push('home');
}
}
}
Проверим эту функцию в 2 тест кейсах:
Если email пустой, то у навигатора не будет вызвана функция
push
.Если email не пустой, то у навигатора будет вызвана функция
push
только один раз.
Для первого случая используем функцию verifyNever()
, чтобы удостовериться, что навигатор не вызывает функцию push
.
test(
'navigator.push should not be called when the email is empty',
() {
// Arrange
String email = '';
// Act
loginViewModel.login(email);
// Assert
verifyNever(() => mockNavigator.push('home'));
},
);
Для второго случая используем функцию verify ().called (1), чтобы проверить, что навигатор вызвал push всего один раз.
verify(() => mockNavigator.push('home')).called(1);
Полный исходный код
Предположим, если обновить код функции login
, чтобы после успешной авторизации он переводил пользователя не только на домашний экран, но и пушил экран профиля.
void login(String email) {
if (email.isNotEmpty) {
navigator.push('home');
navigator.push('profile');
}
}
Тогда нужно обновить и тест тоже.
verify(() => mockNavigator.push('home')).called(2);
Но при запуске теста получаем ошибку.
Была ли функция push
вызвана всего один раз? Нет, один раз была вызвана функция push
с аргументом 'home', но сама функция 'home' вызывается дважды. Есть 2 способа исправления такой ошибки:
Первый способ — вызывать функцию verify
дважды, вместо одного раза:
verify(() => mockNavigator.push('home')).called(1);
verify(() => mockNavigator.push('profile')).called(1);
Второй способ — передать any()
функции verify
:
verify(() => mockNavigator.push(any())).called(2);
Функция «any»
Функция any
используется для сравнения любого значения с определенным типом данных. На примере выше, any()
может соответствовать и 'home', и 'profile'.
В двух исправлениях, упомянутых выше, если передать определенное значение, такое как «home» или «profile», то тест будет более строгим и намного более точным, по сравнению с any()
. Это происходит потому, что any()
соответствует любому значению, которое может быть 'home', 'profile', или любое другое, например, 'login' или 'register'.
Для того, чтобы сделать any()
строже, нужно использовать ее с параметром that
. Параметр that
используется для соответствия любого аргумента, который удовлетворяет определенным условиям.
verify(
() => mockNavigator.push(
any(that: isA()
.having((e) => e.isNotEmpty, 'isNotEmpty', true)),
),
).called(2);
Например, поменяем функцию push
для использования именованных параметров:
void push({
required String screenName,
}) {}
После этого модифицируем тест:
verify(() => mockNavigator.push(screenName: any())).called(2);
Когда запустим тест снова, обнаружим ошибку:
Это потому, что когда используется any()
, как аргумент для именованных параметров, то нужно передать имя этого параметра в параметре named
функции any()
. Конкретно здесь, необходимо вызвать any(named: 'screenName')
вместо any()
.
verify(() => mockNavigator.push(
screenName: any(named: 'screenName'),
)).called(2);
Обратите внимание, что нужно использовать функцию any()
как аргумент к методам when
для stubbing или verify
для верификации. В противном случае, получится ошибка:
Invalid argument(s): The "any" argument matcher is used outside of method
stubbing (via `when`) or verification (via `verify` or `untilCalled`).
Например, когда это используется в функции login
, оно вызовет ошибку.
loginViewModel.login(any());
Функция «captureAny»
Если нужно проверить, что функция push
была вызвана сначала с аргументом «home», а потом с «profile». В таком случае, если вызывать функцию verify
дважды, она будет неспособна проверить, так как нет ничего, что гарантирует, что push('home')
была вызвана раньше, чем push('profile')
.
// BAD
verify(() => mockNavigator.push('home')).called(1);
verify(() => mockNavigator.push('profile')).called(1);
Тогда нужно использовать функцию captureAny
.
test('navigator.push should be called with the correct argument when the email is not empty', () {
// Arrange
String email = 'ntminh@gmail.com';
// Act
loginViewModel.login(email);
// Verify that the navigator.push method is called with the correct argument
final capturedArguments = verify(() =>
mockNavigator.push(captureAny())).captured;
expect(capturedArguments, ['home', 'profile']);
expect(capturedArguments[0], 'home');
expect(capturedArguments[1], 'profile');
});
Функция captureAny
используется для захвата всех значений аргументов. После захвата, можно проверить были ли переданы функции нужные аргументы. Также как функция any()
, у captureAny()
есть 2 параметра — named
и that
, которые по своему функционалу повторяют такие же, как в any()
.
Функция «registerFallbackValue»
Предположим, что теперь обновили функцию push
так, что вместо передачи String, будет передаваться кастомный тип Screen
.
class Navigator {
void push(Screen name) {}
}
class Screen {
final String name;
Screen(this.name);
}
Далее в тесте будет использоваться функция any()
, чтобы представить любое значение Screen
.
verify(() => mockNavigator.push(any())).called(2);
Когда тест запустится, возникнет ошибка.
Есть указания, как исправить ее при помощи функции registerFallbackValue
и объекта с типом Screen
.
setUpAll(() {
registerFallbackValue(Screen('login'));
});
Почему функция any()
кидает ошибку, когда заменяем ей кастомный тип, и что за функция registerFallbackValue
?
Когда используем функции any()
и captureAny()
, Mocktail нужно зарегистрировать значения fallback по умолчанию. Для примитивных типов данных Mocktail сделает это автоматически. Однако, для кастомных типов нужно использовать registerFallbackValue()
, чтобы зарегистрировать значения fallback. Если этого не сделать, то это приведет к ошибке, упомянутой выше.
Нужно вызвать registerFallbackValue()
один раз для каждого типа, чтобы зарегистрировать его fallback значение. Оно будет использоваться во всех тестах. Поэтому лучшим местом для вызова функции registerFallbackValue()
является setUpAll()
.
Полный исходный код
На этом всё! В следующей статье познакомимся с новой техникой, похожей на Mocking — Faking.
Больше про кроссплатформенную разработку в телеграм-канале Flutter. Много.