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)