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/
Добавить комментарий