blocTest does not catch all emitted states

Created on 13 Dec 2019  路  6Comments  路  Source: felangel/bloc

bloc_test does not catch states emitted by consecutive events added inside bloc. Same scenario passes in plain test but fails with blocTest.

I updated your flutter_login sample to replicate the issue.

Updated AuthenticationBloc to add LoggedOut event inside case of LoggedIn

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:flutter_login/authentication/authentication.dart';
import 'package:meta/meta.dart';
import 'package:user_repository/user_repository.dart';

class AuthenticationBloc
    extends Bloc<AuthenticationEvent, AuthenticationState> {
  final UserRepository userRepository;

  AuthenticationBloc({@required this.userRepository})
      : assert(userRepository != null);

  @override
  AuthenticationState get initialState => AuthenticationUninitialized();

  @override
  Stream<AuthenticationState> mapEventToState(
    AuthenticationEvent event,
  ) async* {
    if (event is AppStarted) {
      final bool hasToken = await userRepository.hasToken();

      if (hasToken) {
        yield AuthenticationAuthenticated();
      } else {
        yield AuthenticationUnauthenticated();
      }
    }

    if (event is LoggedIn) {
      yield AuthenticationLoading();
      await userRepository.persistToken(event.token);
      yield AuthenticationAuthenticated();
      add(LoggedOut());
    }

    if (event is LoggedOut) {
      yield AuthenticationLoading();
      await userRepository.deleteToken();
      yield AuthenticationUnauthenticated();
    }
  }
}

In the first case, only first three states are caught by blocTest
Updated tests:

blocTest(
        ' Failing test: emits [uninitialized, loading, authenticated, loading, unauthenticated] when token is persisted',
        build: () => authenticationBloc,
        act: (bloc) => bloc.add(LoggedIn(
              token: 'instance.token',
            )),
        expect: [
          AuthenticationUninitialized(),
          AuthenticationLoading(),
          AuthenticationAuthenticated(),
          AuthenticationLoading(),
          AuthenticationUnauthenticated(),
        ]);

    test(
        'emits [uninitialized, loading, authenticated, loading, unauthenticated] when token is persisted',
        () {
      final expectedResponse = [
        AuthenticationUninitialized(),
        AuthenticationLoading(),
        AuthenticationAuthenticated(),
        AuthenticationLoading(),
        AuthenticationUnauthenticated(),
      ];

      expectLater(
        authenticationBloc,
        emitsInOrder(expectedResponse),
      );

      authenticationBloc.add(LoggedIn(
        token: 'instance.token',
      ));
    });
bloc_test question

Most helpful comment

No problem! If you find anything you feel is lacking please don't hesitate to open an issue and create a pull request 馃憤

Thanks so much! 馃檹

All 6 comments

Hi @MohsinN 馃憢
Thanks for opening an issue!

I'll take a look shortly 馃憤

Hey sorry for the delayed response (just got back from vacation). I took a closer look and this is actually working as expected. blocTest uses emitsExactly under the hood which closes the bloc's sink and stream in order to provide instant feedback about the test (rather than a potential 30 second timeout). In this case, I would not recommend adding another event for the same bloc within mapEventToState as it's unnecessary and redundant.

You can instead refactor the code like:

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:flutter_login/authentication/authentication.dart';
import 'package:meta/meta.dart';
import 'package:user_repository/user_repository.dart';

class AuthenticationBloc
    extends Bloc<AuthenticationEvent, AuthenticationState> {
  final UserRepository userRepository;

  AuthenticationBloc({@required this.userRepository})
      : assert(userRepository != null);

  @override
  AuthenticationState get initialState => AuthenticationUninitialized();

  @override
  Stream<AuthenticationState> mapEventToState(
    AuthenticationEvent event,
  ) async* {
    if (event is AppStarted) {
      final bool hasToken = await userRepository.hasToken();

      if (hasToken) {
        yield AuthenticationAuthenticated();
      } else {
        yield AuthenticationUnauthenticated();
      }
    }

    if (event is LoggedIn) {
      yield AuthenticationLoading();
      await userRepository.persistToken(event.token);
      yield AuthenticationAuthenticated();

      // do logout logic directly or extract into a private helper function
      await userRepository.deleteToken();
      yield AuthenticationUnauthenticated();
    }

    if (event is LoggedOut) {
      yield AuthenticationLoading();
      await userRepository.deleteToken();
      yield AuthenticationUnauthenticated();
    }
  }
}

Hope that helps 馃憤

Hi,

Thank you for the detailed response.

Great work with library. Also, please let me know if I can contribute in any way. I can help out in writing tests if you like. Open to other suggestions as well.

Do let me know what you think.

No problem! If you find anything you feel is lacking please don't hesitate to open an issue and create a pull request 馃憤

Thanks so much! 馃檹

Awesome thanks! 馃槃

@felangel From your comment above:

I would not recommend adding another event for the same bloc within mapEventToState

Is it a general rule? In other words, is it an anti-pattern to invoke add() in mapEventToState()? And, what if add() is invoked on another BLoC object instead?

Thanks in advance for your advice!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

abinvp picture abinvp  路  3Comments

hivesey picture hivesey  路  3Comments

rsnider19 picture rsnider19  路  3Comments

shawnchan2014 picture shawnchan2014  路  3Comments

MahdiPishguy picture MahdiPishguy  路  3Comments