Flutter Bloc Test

Issue

I’m working in a team and we are using flutter bloc for state management, however one of our test cases just doesn’t make any sense at all. The bloc itself works fine but the test is what’s not making any sense.

Below is the bloc itself.

import 'package:vaccify/features/logout/logout.dart';
import 'package:vaccify/features/side_navigation_bar/domain/get_user_name.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:vaccify/features/side_navigation_bar/domain/get_user_profile_pic_url.dart';

part 'auth_event.dart';
part 'auth_state.dart';

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  GetUserName getUserName;
  GetUserProfilePic getUserProfilePic;
  Logout logout;

  AuthBloc(
      {required this.getUserName,
      required this.logout,
      required this.getUserProfilePic})
      : super(NotLoggedIn()) {
    on<AuthLogIn>(_onLogIn);
    on<AuthLogOut>(_onLogOut);
  }

  void _onLogIn(
    AuthLogIn event,
    Emitter<AuthState> emit,
  ) async {
    final name = await getUserName();
    final profilePic = await getUserProfilePic();
    emit(LoggedIn(name, profilePic));
  }

  void _onLogOut(
    AuthLogOut event,
    Emitter<AuthState> emit,
  ) {
    logout();
    emit(NotLoggedIn());
  }
}

And now for the test

import 'package:bloc_test/bloc_test.dart';
import 'package:vaccify/core/bloc/authentication/auth_bloc.dart';
import 'package:vaccify/features/logout/logout.dart';
import 'package:vaccify/features/side_navigation_bar/domain/get_user_name.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:vaccify/features/side_navigation_bar/domain/get_user_profile_pic_url.dart';

class MockGetUserName extends Mock implements GetUserName {}
class MockGetProfilePicUrl extends Mock implements GetUserProfilePic {}
class MockLogout extends Mock implements Logout {}

void main() {
  late AuthBloc authBloc;
  late MockGetUserName mockGetUserName;
  late MockLogout mockLogout;
  late MockGetProfilePicUrl mockGetProfilePicUrl;

  setUp(() {
    mockGetUserName = MockGetUserName();
    mockLogout = MockLogout();
    mockGetProfilePicUrl = MockGetProfilePicUrl();

    authBloc = AuthBloc(
        getUserName: mockGetUserName,
        logout: mockLogout,
        getUserProfilePic: mockGetProfilePicUrl);
  });

  /// Contains bloc tests for the [AuthBloc] class
  group(
    'AuthBloc',
    () {
      /// Tests that a [LoggedIn] state occurs when the [AuthLogIn] is called.
      blocTest(
        'login',
        build: () {
          when(() => mockGetUserName()).thenAnswer((_) async => "Hello");
          when(() => mockGetProfilePicUrl()).thenAnswer((_) async => "");
          return authBloc;
        },
        act: (AuthBloc bloc) {
          bloc.add(AuthLogIn());
        },
        expect: () => [isA<LoggedIn>()],
      );

      /// Tests that a [NotLoggedIn] state occurs when the [AuthLogOut] is called.
      blocTest(
        'logout',
        build: () {
          when(() => mockGetUserName()).thenAnswer((_) async => "John Smith");
          when(() => mockGetProfilePicUrl()).thenAnswer((_) async => "");
          when(() => mockLogout()).thenAnswer((_) async {});
          return authBloc;
        },
        act: (AuthBloc bloc) {
          bloc.add(AuthLogIn());
          bloc.add(AuthLogOut());
        },

        expect: () => [
          isA<LoggedIn>(),
          isA<NotLoggedIn>(),
        ],
      );
    },
  );
}

The first test ‘login’ works fine and passes because we add one event "AuthLogin()" and then we expect LoggedIn state

The second test ‘logout’ is the problem because we are adding two events AuthLogin() & AuthLogOut() because you have to be logged in to be able to logout.
Then we expect LoggedIn & NotLoggedIn states respectively.

The test fails with the following message

Expected: [<<Instance of ‘LoggedIn’>>, <<Instance of ‘NotLoggedIn’>>]

Actual: [Instance of ‘NotLoggedIn’, Instance of ‘LoggedIn’]

Interestingly when we swap the two expects the test passes…Like below

expect: () => [
  isA<NotLoggedIn>(),
  isA<LoggedIn>(),
],

Any advice or guidance will be greatly appreciated.

Thanks all

Solution

If you’re adding AuthLogIn event just to prepare your bloc for the second event, instead of that you can use seed from blocTest. Something like this:

blocTest(
  'logout',
  build: () {
    when(() => mockLogout()).thenAnswer((_) async {});
    return authBloc;
  },
  seed: () => LoggedIn(),
  act: (AuthBloc bloc) {
    bloc.add(AuthLogOut());
  },
  expect: () => [
    isA<NotLoggedIn>(),
  ],
);

And about your problem, I think using a delay between adding two events (await Future.delayed(Duration(seconds: 1))) will fix it. Note that Bloc transforms events concurrently by default and in your case the log out is being processed first (it finishes faster than log in function) and then your log in event is being processed which causes the problem.

Answered By – Amir_P

Answer Checked By – David Goodson (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *