Тестирование BLoC

от автора

Hola, Amigos! На связи Павел Гершевич, Mobile Team Lead агентства продуктовой разработки Amiga и соавтор Flutter. Много. Недавно мы перевели для вас серию статей про модульное тестирование, но одна важная тема осталась за бортом. Сегодня познакомимся с тестированием BLoC при помощи модульных тестов.

Тестируем создание BLoC

Допустим, мы пишем экран входа в приложение по email и паролю и используем библиотеку flutter_bloc для управления состоянием. Тогда у нас будут такие состояния:

@immutable abstract class LoginState {}  class LoginInitialState extends LoginState {}  class LoginDataState extends LoginState {   final String? email;   final String? password;    ... }  class LoginLoadingState extends LoginState {   final String? email;   final String? password;    ... }  class LoginSuccessState extends LoginState {}  class LoginErrorState extends LoginState {   final String? email;   final String? password;   final String? errorToShow;    ... }

И конструктор нашего BLoC будет выглядеть так:

class LoginBloc extends Bloc<LoginEvent, LoginState> {   final LoginRepository _loginRepository;     LoginBloc(this._loginRepository) : super(LoginInitialState()) {     ...   } }

В процессе тестирования, будем дополнять его логикой и взаимодействиями с другими классами.

Давайте теперь напишем первый тест, который будет проверять, что при создании BLoC, он имеет состояние LoginInitialState. Но сначала нужно создать сам BLoC. Для этого подготовим Mock-объект репозитория при помощи библиотеки mocktail, он будет создаваться всего один раз. А вот сам BLoC мы будет создавать для каждого теста единожды.

LoginRepository repository; LoginBloc bloc;   setUp(() {   repository = MockLoginRepository();   bloc = LoginBloc(repository); });

Далее напишем тест. Для этого нам нужно получить состояние из только что созданного BLoC и проверить его тип при помощи Matcher isA.

test('LoginBloc should be initialized with LoginInitialState', () {   // act   final state = bloc.state;     // assert   expect(state, isA<LoginInitialState>()); });

Теперь мы можем приступить к тестированию логики внутри BLoC.

Простые модульные тесты

Для того, чтобы нам и дальше тестировать BLoC, необходимо создать события и написать под это логику. Сделаем события для ввода email и пароля.

@immutable abstract class LoginEvent {}   class EditedEmail extends LoginEvent {   final String email;   EditedEmail(this.email); }   class EditedPassword extends LoginEvent {   final String password;   EditedPassword(this.password); }

Также у состояний нам понадобятся геттеры для email и password. Для этого воспользуемся расширениями.

extension LoginStateX on LoginState {   String? get emailStr {     if (this is LoginInitialState || this is LoginSuccessState) {       return null;     } else if (this is LoginDataState) {       return (this as LoginDataState).email;     } else if (this is LoginLoadingState) {       return (this as LoginLoadingState).email;     } else if (this is LoginErrorState) {       return (this as LoginErrorState).email;     }     return null;   } }

Напишем обработку данных событий.

on<EditedEmail>((event, emit) {   emit(LoginDataState(     email: event.email,      password: state.passwordStr,   )); });   on<EditedPassword>((event, emit) {   emit(LoginDataState(     email: state.emailStr,     password: event.password,    )); });

Приступим к тестированию, но стандартная библиотека для этого уже не подойдет. Нам нужен пакет bloc_test от создателей flutter_bloc.

Когда мы его поставили, можно перейти к самим тестам.

blocTest(   'emits [LoginDataState] after adding email',   build: () => bloc,   act: (_bloc) => _bloc.add(EditedEmail('example@sample.com')),   expect: () => [     isA<LoginDataState>(),   ], );

В этом тесте нужно передать в build наш BLoC, созданный ранее. Его также можно создавать и в самом параметре. На самом деле, это наш шаг Arrange из методологии написания тестов AAA. Далее идет место для действий — act, который соответствует одноименному шагу, и expect для проверки того, что придет в BLoC.

Давайте добавим тест для события — ввод пароля.

blocTest(   'emits [LoginDataState] after adding password',   build: () => bloc,   act: (_bloc) => _bloc.add(EditedPassword('myPass123')),   expect: () => [     isA<LoginDataState>(),   ], );

Тесты для сложного события

Есть событие самого входа:

class LoginButtonPressed extends LoginEvent {}

И его обработка:

on<LoginButtonPressed>((event, emit) async {   emit(LoginLoadingState(     email: state.emailStr,     password: state.passwordStr,   ));   if (state.emailStr?.isNotEmpty == false ||     state.emailStr?.isNotEmpty == false) {       emit(LoginErrorState(         email: state.emailStr,         password: state.passwordStr,         errorToShow: 'Email or password is empty',       ));     return;   }     try {     await _loginRepository.login(       email: state.emailStr,       password: state.passwordStr,     );     emit(LoginSuccessState());   } catch (_) {     emit(LoginErrorState(       email: state.emailStr,       password: state.passwordStr,       errorToShow: 'Server error',     ));   } });

Если мы внимательно посмотрим на код, то увидим, что нужно протестировать следующие кейсы:

  • Когда email пустой, получаем LoginErrorState с ошибкой “Email or password is empty”

  • Когда пароль пустой, получаем LoginErrorState с ошибкой “Email or password is empty”

  • Когда email и пароль пустые, получаем LoginErrorState с ошибкой “Email or password is empty”

  • Когда все прошло успешно, получаем LoginSuccessState

  • Если произошла ошибка где-то в репозитории, получаем LoginErrorState с ошибкой “Server error”

Также стоит отметить, что в каждом из этих случаев будет добавляться событие LoginLoadingState.

Давайте напишем тест для первого случая, второй и третий будут аналогичны ему.

blocTest(   'emits [LoginErrorState] if email is null',   build: () => bloc,   seed: () => LoginDataState(     email: null,     password: 'myPass123',   ) as LoginState,   act: (_bloc) => _bloc.add(LoginButtonPressed()),   expect: () => [     isA<LoginLoadingState>(),     isA<LoginErrorState>(),   ], );

Тут мы использовали еще одно свойство blocTest — seed, которое нужно для подстановки изначального состояния в BLoC. Таким образом, не требуется дополнительно вызывать все методы, иначе тест выглядел бы так:

blocTest(   'emits [LoginErrorState] if email is null',   build: () => bloc,   act: (_bloc) {     _bloc.add(EditedPassword('myPass123'));     _bloc.add(LoginButtonPressed());   },   expect: () => [     isA<LoginDataState>(),     isA<LoginLoadingState>(),     isA<LoginErrorState>(),   ], );

И мы бы не были точно уверены, что все события обработаются как надо.

Далее проверим успешный вход.

blocTest('emits [LoginSuccessState]',   build: () {     when(() => repository.login(       email: any(named: 'email'),       password: any(named: 'password'),     )).thenAnswer((_) => Future.value(true));     return bloc;   },   seed: () => LoginDataState(     email: 'example@sample.com',     password: 'myPass123',   ) as LoginState,   act: (_bloc) => _bloc.add(LoginButtonPressed()),   expect: () => [     isA<LoginLoadingState>(),     isA<LoginSuccessState>(),   ],   verify: (_) {     verify(() => repository.login(       email: any(named: 'email'),       password: any(named: 'password'),     )).called(1);   }); });

Полный код можно посмотреть здесь

Из примера выше видно, что в параметре build, перед тем, как вернуть BLoC, используется Stubbing для функции login. Далее все как и в прошлых тестах, за исключением того, что появился параметр verify. Это функция, которая позволит вызывать verify из библиотеки mocktail.

Что еще умеет blocTest?

В примере выше мы рассмотрели не все возможности библиотеки bloc_test. У метода blocTest есть еще несколько параметров:

  • setUp — функция, с помощью которой создаются или пересоздаются зависимости, но рекомендуется делать это в методе setUp из flutter_test

  • tearDown — функция, с помощью которой обнуляются зависимости, но рекомендуется делать это в методе tearDown из flutter_test

  • wait — параметр, который принимает Duration и после вызова функции act ожидает переданное ему время перед тем, как начать отслеживание состояний

  • skip — параметр, который показывает, сколько нужно пропустить состояний вначале.
    Например, из кейса, где мы сначала добавляем пароль, а потом нажимаем на кнопку, можно сделать skip равным 1 и не проверять, что пароль закинулся.

  • errors — функция аналогичная expect, но для проверки исключений, которые были выброшены во время работы BLoC. Например, если в add передать null вместо события, или где-то в обработчике попалась необработанная ошибка.

Заключение

В этой статье мы рассмотрели, как можно написать Unit-тесты, чтобы протестировать BLoC в наших Flutter-приложениях.

Всем хорошего кода!

Подписывайтесь на наш авторский телеграм-канал Flutter.Много, чтобы всегда все новости узнавать первыми! 


ссылка на оригинал статьи https://habr.com/ru/articles/837646/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *