felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.77k stars 3.39k forks source link

fix: expected: [<States>], actual [] when unit testing landing page cubit #4111

Closed Jeastwood1995 closed 5 months ago

Jeastwood1995 commented 6 months ago

I've just recently implemented a landing page in my app that verified whether an email address has been confirmed. There isn't much too it as it only checks the existence of a few parameters sent. The only actual method that can be called from the screen is when the email has been successfully confirmed, then just redirect them to the login page. All of the other logic that is calling the relevant states is being handled via the constructor of the cubit like so:

class ConfirmEmailCubit extends Cubit<ConfirmEmailState> {
  final CustomNavigatorService _customNavigatorService;
  final Map<String, dynamic> _queryParams;
  final Logger _logger;

  ConfirmEmailCubit(this._customNavigatorService, this._queryParams, this._logger) : super(ConfirmEmailLoading()) {
    _handleResponse();
  }

  void _handleResponse() {
    if (_queryParams.containsKey('code')) {
      _handleConfirmedRedirect();
    } else if (_queryParams.containsKey('error') || _queryParams.containsKey('error_description')) {
      _handleErrorRedirect();
    } else {
      emit(ConfirmEmailError(S.current.no_direct_access));
    }
  }

  void _handleConfirmedRedirect() {
    final code = _queryParams['code'].toString();

    if (code.isEmpty) {
      _logger.warning('No code passed with request');
      emit(ConfirmEmailError(S.current.internal_server_error));
    } else {
      emit(ConfirmEmailConfirmed());
    }
  }

  void _handleErrorRedirect() {
    final errorCode = _queryParams['error'].toString();
    final errorMessage = _queryParams['error_description'].toString();

    if (errorCode.isNotEmpty && errorMessage.isNotEmpty) {
      emit(ConfirmEmailError(errorMessage));
    } else {
      _logger.warning('No error code or message passed with request');
      emit(ConfirmEmailError(S.current.internal_server_error));
    }
  }

  void navigateToLogin() {
    _customNavigatorService.navigateToScreenAndRemoveCurrentScreen(CustomRoutes.login);
  }
}

I'm just making a simple test for when no parameters are being in. Unfortunately the test says that there is nothing being returned, even though in reality it works as expected in the app. Because the actual state I need to test is handled by the construictor, then the act method does nothing.

blocTest(
      'Confirm Email with no params',
      build: () => ConfirmEmailCubit(
        mockNavigatorService,
        {},
        mockLogger,
      ),
      expect: () => <ConfirmEmailState>[
        ConfirmEmailLoading(),
        ConfirmEmailError(S.current.no_direct_access),
      ],
      act: (ConfirmEmailCubit cubit) => null,
    );

I've just changed this so I can call this via a method and the expect is returning the expected states. It looks like the expected results only function when you do call a valid method. However in this case it't not necessary since it's a landing page that isn't calling any methods in the test above. Can you please look into this?

fernan542 commented 6 months ago

Adding a method in constructor makes things harder to test. The suggested way is to call it via initializer or other way. Can you provide an accessible repo to inspect it more? Thanks!

Jeastwood1995 commented 6 months ago

I've updated it to the following, the handleRequest is now a public method and this will only fire when not instantiated via the test:

blocTest<ConfirmEmailCubit, ConfirmEmailState>(
      'Confirm Email with no params',
      build: () => ConfirmEmailCubit(
        mockNavigatorService,
        {},
        mockLogger,
        testMode: true,
      ),
      seed: () => ConfirmEmailLoading(),
      expect: () => <ConfirmEmailState>[
        ConfirmEmailError(S.current.no_direct_access),
      ],
      act: (ConfirmEmailCubit cubit) => cubit.handleResponse(),
    );
class ConfirmEmailCubit extends Cubit<ConfirmEmailState> {
  final CustomNavigatorService _customNavigatorService;
  final Map<String, dynamic> _queryParams;
  final Logger _logger;
  final bool testMode;

  ConfirmEmailCubit(
    this._customNavigatorService,
    this._queryParams,
    this._logger, {
    this.testMode = false,
  }) : super(ConfirmEmailLoading()) {
    if (!testMode) {
      handleResponse();
    }
  }

  void handleResponse() {
    if (_queryParams.containsKey('code')) {
      _handleConfirmedRedirect();
    } else if (_queryParams.containsKey('error') || _queryParams.containsKey('error_description')) {
      _handleErrorRedirect();
    } else {
      emit(ConfirmEmailError(S.current.no_direct_access));
    }
  }

  void _handleConfirmedRedirect() {
    final code = _queryParams['code'].toString();

    if (code.isEmpty) {
      _logger.warning('No code passed with request');
      emit(ConfirmEmailError(S.current.internal_server_error));
    } else {
      emit(ConfirmEmailConfirmed());
    }
  }

  void _handleErrorRedirect() {
    final errorCode = _queryParams['error'].toString();
    final errorMessage = _queryParams['error_description'].toString();

    if (errorCode.isNotEmpty && errorMessage.isNotEmpty) {
      emit(ConfirmEmailError(errorMessage));
    } else {
      _logger.warning('No error code or message passed with request');
      emit(ConfirmEmailError(S.current.internal_server_error));
    }
  }

  void navigateToLogin() {
    _customNavigatorService.navigateToScreenAndRemoveCurrentScreen(CustomRoutes.login);
  }
}
felangel commented 5 months ago

Hi @Jeastwood1995 👋 Thanks for opening an issue!

Have you resolved this? If not, are you able to share a link to a minimal reproduction sample? Thanks!

felangel commented 5 months ago

Going to close this for now but if it's still an issue please let me know and provide a minimal reproduction sample and I'm happy to take a closer look 👍