[Перевод] Полный гайд по тестированию на Flutter. Часть 5: Mocktail

Hola, Amigos! На связи Павел Гершевич, Mobile Team Lead агентства продуктовой разработки Amiga. В предыдущих статьях, мы разобрали, как использовать библиотеку Mocktail для техник mocking и stubbing в Unit-тестах. Сегодня погрузимся глубже в ее изучение. Оригинал перевода по ссылке. Все выпуски на моей странице. Поехали!

da3f0af9514fdc34194cc1e1cdf0e1eb.png

Чтобы не пропускать новые выпуски, подписывайтесь на наш авторский телеграм-канал 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);

Но при запуске теста получаем ошибку.

a4fd3cb9e2327324504ee87e413932ee.png

Была ли функция 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);

Когда запустим тест снова, обнаружим ошибку:

ad87ab7417a4c44ccf8e1cf43a8b678e.png

Это потому, что когда используется 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);

Когда тест запустится, возникнет ошибка.

81a329b3e01a9ca7ca36740d7b8123c9.png

Есть указания, как исправить ее при помощи функции registerFallbackValue и объекта с типом Screen.

setUpAll(() {
  registerFallbackValue(Screen('login'));
});

Почему функция any() кидает ошибку, когда заменяем ей кастомный тип, и что за функция registerFallbackValue?

Когда используем функции any() и captureAny(), Mocktail нужно зарегистрировать значения fallback по умолчанию. Для примитивных типов данных Mocktail сделает это автоматически. Однако, для кастомных типов нужно использовать registerFallbackValue(), чтобы зарегистрировать значения fallback. Если этого не сделать, то это приведет к ошибке, упомянутой выше.

Нужно вызвать registerFallbackValue() один раз для каждого типа, чтобы зарегистрировать его fallback значение. Оно будет использоваться во всех тестах. Поэтому лучшим местом для вызова функции registerFallbackValue() является setUpAll().

Полный исходный код

На этом всё! В следующей статье познакомимся с новой техникой, похожей на Mocking — Faking.

Больше про кроссплатформенную разработку в телеграм-канале Flutter. Много.

© Habrahabr.ru