lundi 21 décembre 2020

Flutter Bloc Testing not catching thenThrow WebException

I'm trying to test the BLoCs in my Flutter App but I hit a wall with this problem below.

===== asynchronous gap ===========================
dart:async                                                 _AsyncAwaitCompleter.completeError
package:bloc_test/src/bloc_test.dart                       runBlocTest.<fn>
dart:async                                                 runZoned
package:bloc_test/src/bloc_test.dart 157:9                 runBlocTest
package:bloc_test/src/bloc_test.dart 127:11                blocTest.<fn>

Expected: [
            ChangePasswordLoading:ChangePasswordLoading,
            ChangePasswordFailure:ChangePasswordFailure
          ]
  Actual: [
            ChangePasswordLoading:ChangePasswordLoading,
            ChangePasswordSuccess:ChangePasswordSuccess
          ]
   Which: at location [1] is ChangePasswordSuccess:<ChangePasswordSuccess> instead of ChangePasswordFailure:<ChangePasswordFailure>

package:test_api                             expect
package:bloc_test/src/bloc_test.dart 176:9   runBlocTest.<fn>
===== asynchronous gap ===========================
dart:async                                   _asyncThenWrapperHelper
package:bloc_test/src/bloc_test.dart         runBlocTest.<fn>
dart:async                                   runZoned
package:bloc_test/src/bloc_test.dart 157:9   runBlocTest
package:bloc_test/src/bloc_test.dart 127:11  blocTest.<fn>

which is caused by this Failing BLoC test

blocTest<ChangePasswordBloc, ChangePasswordState>(
      'emits [ChangePasswordLoading, ChangePasswordFailure] on failed ChangePassword',
      build: () {
        when(authenticationRepository.changePassword(
          'token',
          'oldPassword',
          'newPassword',
          'newPasswordConfirm',
        )).thenThrow(WebException(403));
        return changePasswordBloc;
      },
      act: (bloc) => bloc
        ..add(ChangePassword(
          oldPassword: 'oldPassword',
          newPassword: 'newPassword',
          newPasswordConfirm: 'newPasswordConfirm',
        )),
      expect: [
        ChangePasswordLoading(),
        ChangePasswordFailure(error: 'Old password is not correct'),
      ],
      errors: [isA<WebException>()],
    );

This is the code I have used to test my ChangePasswordBloc (Notice all the other tests are passing successfully)

import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_app/business_logic/blocs/change_password/change_password_bloc.dart';
import 'package:flutter_app/business_logic/blocs/change_password/change_password_event.dart';
import 'package:flutter_app/business_logic/blocs/change_password/change_password_state.dart';
import 'package:flutter_app/data/exceptions/web_exception.dart';
import 'package:flutter_app/data/repositories/authentication_repository.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

class MockAuthenticationRepository extends Mock
    implements AuthenticationRepository {}

class MockSecureStorage extends Mock implements FlutterSecureStorage {}

main() {
  ChangePasswordBloc changePasswordBloc;
  MockSecureStorage secureStorage;
  MockAuthenticationRepository authenticationRepository;

  setUp(() {
    secureStorage = MockSecureStorage();
    authenticationRepository = MockAuthenticationRepository();
    changePasswordBloc = ChangePasswordBloc(
      authenticationRepository,
      secureStorage,
    );
  });

  tearDown(() {
    changePasswordBloc?.close();
  });

  test(
    'initial state is ChangePasswordInitial',
    () => expect(changePasswordBloc.state, ChangePasswordInitial()),
  );

  group('ChangePassword process', () {
    blocTest<ChangePasswordBloc, ChangePasswordState>(
      'emits [ChangePasswordLoading, ChangePasswordSuccess] on successful ChangePassword',
      build: () {
        when(authenticationRepository.changePassword(
          'token',
          'oldPassword',
          'newPassword',
          'newPasswordConfirm',
        )).thenAnswer((_) async => null);
        return changePasswordBloc;
      },
      act: (bloc) => bloc
        ..add(ChangePassword(
          oldPassword: 'oldPassword',
          newPassword: 'newPassword',
          newPasswordConfirm: 'newPasswordConfirm',
        )),
      expect: [
        ChangePasswordLoading(),
        ChangePasswordSuccess(),
      ],
    );

    blocTest<ChangePasswordBloc, ChangePasswordState>(
      'emits [ChangePasswordLoading, ChangePasswordFailure] on failed ChangePassword',
      build: () {
        when(authenticationRepository.changePassword(
          'token',
          'oldPassword',
          'newPassword',
          'newPasswordConfirm',
        )).thenThrow(WebException(403));
        return changePasswordBloc;
      },
      act: (bloc) => bloc
        ..add(ChangePassword(
          oldPassword: 'oldPassword',
          newPassword: 'newPassword',
          newPasswordConfirm: 'newPasswordConfirm',
        )),
      expect: [
        ChangePasswordLoading(),
        ChangePasswordFailure(error: 'Old password is not correct'),
      ],
      errors: [isA<WebException>()],
    );
  });
}

This is my ChangePasswordBloc code

ChangePasswordBloc

import 'dart:async';
import 'package:flutter_app/data/exceptions/web_exception.dart';
import 'package:flutter_app/data/repositories/authentication_repository.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:bloc/bloc.dart';
import 'change_password_event.dart';
import 'change_password_state.dart';

class ChangePasswordBloc
    extends Bloc<ChangePasswordEvent, ChangePasswordState> {
  final AuthenticationRepository _authenticationRepository;
  final FlutterSecureStorage _secureStorage;

  ChangePasswordBloc(AuthenticationRepository authenticationRepository,
      FlutterSecureStorage secureStorage)
      : _authenticationRepository = authenticationRepository,
        _secureStorage = secureStorage,
        super(ChangePasswordInitial());

  @override
  Stream<ChangePasswordState> mapEventToState(
    ChangePasswordEvent event,
  ) async* {
    if (event is ChangePassword) {
      yield* _mapChangePasswordToState(event);
    }
  }

  Stream<ChangePasswordState> _mapChangePasswordToState(
      ChangePassword event) async* {
    yield ChangePasswordLoading();
    try {
      final accessToken = await _secureStorage.read(key: 'accessToken');

      await _authenticationRepository.changePassword(
        accessToken,
        event.oldPassword,
        event.newPassword,
        event.newPasswordConfirm,
      );
      yield ChangePasswordSuccess();
    } on WebException catch (e) {
      String errorMessage;
      if (e.statusCode == 422) {
        errorMessage = 'Password must be 8 characters long';
      } else if (e.statusCode == 419) {
        errorMessage = 'New Password is not matching';
      } else if (e.statusCode == 403) {
        errorMessage = 'Old password is not correct';
      }
      yield ChangePasswordFailure(error: errorMessage ?? e.toString());
    } catch (err) {
      yield ChangePasswordFailure(
          error: err.toString() ?? 'An unknown error occurred');
    }
  }
}

As you can tell, if a WebException is thrown, I yield ChangePasswordFailure() with an error message. This does work on the actual app, so I am certain that logic works, yet the test does not seem to catch that thrown WebException.

ChangePasswordEvent

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

abstract class ChangePasswordEvent extends Equatable {
  @override
  List<Object> get props => [];
}

class ChangePassword extends ChangePasswordEvent {
  final String oldPassword;
  final String newPassword;
  final String newPasswordConfirm;

  ChangePassword({
    @required this.oldPassword,
    @required this.newPassword,
    @required this.newPasswordConfirm,
  });

  @override
  List<Object> get props => [oldPassword, newPassword, newPasswordConfirm];
}

ChangePasswordState

import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';

abstract class ChangePasswordState extends Equatable {
  @override
  List<Object> get props => [];
}

class ChangePasswordInitial extends ChangePasswordState {}

class ChangePasswordLoading extends ChangePasswordState {}

class ChangePasswordSuccess extends ChangePasswordState {}

class ChangePasswordFailure extends ChangePasswordState {
  final String error;

  ChangePasswordFailure({@required this.error});

  @override
  List<Object> get props => [error];
}

Any suggestions or advice as to why .thenThrow(WebException(403)) is not actually being caught when it actually works on the real Flutter App (if a WebException is thrown, ChangePasswordFailure is always thrown)?

I have another example with the same code which does work (The code for ClientInfoBloc is handles WebExceptions in the same way as ChangePasswordBloc and it also works in the real Flutter app)

Working Test Example with thrown WebException

I checked this related issue but it did not fix anything.

Aucun commentaire:

Enregistrer un commentaire