Testing Flutter BLoC and Cubit with bloc_test
The bloc_test package provides blocTest() — a declarative test helper that makes BLoC and Cubit testing concise. You define the initial state, the events/actions to trigger, and the expected state sequence. Compared to manually subscribing to BLoC streams, blocTest() handles stream subscription, async operations, and teardown automatically.
Key Takeaways
blocTest() follows a given/when/then structure. build creates the BLoC, act triggers events or actions, expect lists the expected states emitted.
States are compared by value, not reference. Use Equatable on your state classes (or implement == manually) so blocTest() can compare states correctly.
seed sets the initial state before the test. Without seed, the test starts from the BLoC's initial state. Use seed when testing behavior from a specific intermediate state.
errors tests that BLoC throws. If the BLoC emits an error (via addError or an uncaught exception), assert on it with the errors parameter.
verify runs after all states are emitted. Use it to verify that mock methods were called, like confirming the repository method was invoked with the right argument.
Why bloc_test?
Testing a BLoC manually involves subscribing to its stream, waiting for states, and cleaning up. It's verbose and error-prone:
// Manual BLoC testing (verbose)
test('emits [Loading, Success] when LoadUsers is added', () async {
final userBloc = UserBloc(repository: mockRepo);
final states = <UserState>[];
final subscription = userBloc.stream.listen(states.add);
userBloc.add(LoadUsers());
await Future.delayed(const Duration(milliseconds: 100));
expect(states, [
isA<UserLoading>(),
isA<UserSuccess>(),
]);
await subscription.cancel();
await userBloc.close();
});With bloc_test, the same test becomes:
blocTest<UserBloc, UserState>(
'emits [Loading, Success] when LoadUsers is added',
build: () => UserBloc(repository: mockRepo),
act: (bloc) => bloc.add(LoadUsers()),
expect: () => [isA<UserLoading>(), isA<UserSuccess>()],
);The blocTest() function handles subscription, waiting, and teardown.
Installation
# pubspec.yaml
dev_dependencies:
bloc_test: ^9.1.7
mocktail: ^1.0.4
flutter_test:
sdk: flutterTesting a Cubit
Cubits are simpler than BLoCs — they expose methods instead of events. Testing is identical:
// lib/cubits/counter_cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() { if (state > 0) emit(state - 1); }
void reset() => emit(0);
}// test/cubits/counter_cubit_test.dart
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/cubits/counter_cubit.dart';
void main() {
group('CounterCubit', () {
blocTest<CounterCubit, int>(
'emits [1] when increment is called',
build: () => CounterCubit(),
act: (cubit) => cubit.increment(),
expect: () => [1],
);
blocTest<CounterCubit, int>(
'emits [1, 2] when increment is called twice',
build: () => CounterCubit(),
act: (cubit) {
cubit.increment();
cubit.increment();
},
expect: () => [1, 2],
);
blocTest<CounterCubit, int>(
'does not emit below 0',
build: () => CounterCubit(),
act: (cubit) => cubit.decrement(),
expect: () => [], // no states emitted
);
blocTest<CounterCubit, int>(
'resets to 0',
build: () => CounterCubit(),
seed: () => 5, // start from 5
act: (cubit) => cubit.reset(),
expect: () => [0],
);
});
}Testing a BLoC with Events
// lib/blocs/user_bloc.dart
abstract class UserEvent {}
class LoadUsers extends UserEvent {}
class DeleteUser extends UserEvent {
final String userId;
DeleteUser(this.userId);
}
abstract class UserState extends Equatable {
@override List<Object> get props => [];
}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserSuccess extends UserState {
final List<User> users;
UserSuccess(this.users);
@override List<Object> get props => [users];
}
class UserError extends UserState {
final String message;
UserError(this.message);
@override List<Object> get props => [message];
}
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository _repository;
UserBloc({required UserRepository repository})
: _repository = repository,
super(UserInitial()) {
on<LoadUsers>(_onLoadUsers);
on<DeleteUser>(_onDeleteUser);
}
Future<void> _onLoadUsers(LoadUsers event, Emitter<UserState> emit) async {
emit(UserLoading());
try {
final users = await _repository.getAllUsers();
emit(UserSuccess(users));
} catch (e) {
emit(UserError(e.toString()));
}
}
Future<void> _onDeleteUser(DeleteUser event, Emitter<UserState> emit) async {
try {
await _repository.deleteUser(event.userId);
final users = await _repository.getAllUsers();
emit(UserSuccess(users));
} catch (e) {
emit(UserError('Failed to delete user'));
}
}
}// test/blocs/user_bloc_test.dart
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:my_app/blocs/user_bloc.dart';
import 'package:my_app/models/user.dart';
import 'package:my_app/repositories/user_repository.dart';
class MockUserRepository extends Mock implements UserRepository {}
void main() {
late MockUserRepository mockRepo;
setUp(() {
mockRepo = MockUserRepository();
});
group('UserBloc', () {
final users = [
User(id: '1', name: 'Alice', email: 'alice@example.com'),
User(id: '2', name: 'Bob', email: 'bob@example.com'),
];
blocTest<UserBloc, UserState>(
'emits [Loading, Success] on LoadUsers',
build: () {
when(() => mockRepo.getAllUsers()).thenAnswer((_) async => users);
return UserBloc(repository: mockRepo);
},
act: (bloc) => bloc.add(LoadUsers()),
expect: () => [
UserLoading(),
UserSuccess(users),
],
);
blocTest<UserBloc, UserState>(
'emits [Loading, Error] when repository throws',
build: () {
when(() => mockRepo.getAllUsers()).thenThrow(Exception('Network error'));
return UserBloc(repository: mockRepo);
},
act: (bloc) => bloc.add(LoadUsers()),
expect: () => [
UserLoading(),
isA<UserError>().having((s) => s.message, 'message', contains('Network error')),
],
);
blocTest<UserBloc, UserState>(
'emits updated list after DeleteUser',
build: () {
when(() => mockRepo.deleteUser('1')).thenAnswer((_) async {});
when(() => mockRepo.getAllUsers()).thenAnswer(
(_) async => [users[1]], // Alice deleted
);
return UserBloc(repository: mockRepo);
},
seed: () => UserSuccess(users),
act: (bloc) => bloc.add(DeleteUser('1')),
expect: () => [
UserSuccess([users[1]]),
],
verify: (_) {
verify(() => mockRepo.deleteUser('1')).called(1);
verify(() => mockRepo.getAllUsers()).called(1);
},
);
});
}Using seed
seed lets you start a test from a specific state:
blocTest<UserBloc, UserState>(
'deletes user when already in success state',
build: () => UserBloc(repository: mockRepo),
seed: () => UserSuccess([
User(id: '1', name: 'Alice', email: 'alice@example.com'),
]),
act: (bloc) => bloc.add(DeleteUser('1')),
// ...
);Without seed, you'd need to first emit LoadUsers to get to UserSuccess before testing deletion.
Testing Waiting
For BLoCs with delays or debounce:
blocTest<SearchBloc, SearchState>(
'emits results after debounce delay',
build: () => SearchBloc(repository: mockRepo),
act: (bloc) => bloc.add(SearchQueryChanged('flutter')),
wait: const Duration(milliseconds: 300), // wait for debounce
expect: () => [
SearchLoading(),
SearchSuccess(results: ['Flutter Testing', 'Flutter BLoC']),
],
);Testing Errors
blocTest<UserBloc, UserState>(
'does not emit states but adds error when...',
build: () => UserBloc(repository: mockRepo),
act: (bloc) {
// Trigger an error by calling addError directly (rare, for testing error handling)
bloc.addError(Exception('Unexpected error'), StackTrace.current);
},
errors: () => [isA<Exception>()],
);Widget Tests with BLoC
Providing a pre-seeded BLoC to a widget test:
testWidgets('UserList shows users from BLoC', (tester) async {
final mockRepo = MockUserRepository();
when(() => mockRepo.getAllUsers()).thenAnswer(
(_) async => [User(id: '1', name: 'Alice', email: 'alice@example.com')],
);
await tester.pumpWidget(
MaterialApp(
home: BlocProvider(
create: (_) => UserBloc(repository: mockRepo)..add(LoadUsers()),
child: const UserList(),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Alice'), findsOneWidget);
});Or use a pre-configured BLoC:
await tester.pumpWidget(
BlocProvider.value(
value: UserBloc(repository: mockRepo)..add(LoadUsers()),
child: const MaterialApp(home: UserList()),
),
);Running BLoC Tests
flutter test <span class="hljs-built_in">test/blocs/ <span class="hljs-comment"># all BLoC tests
flutter <span class="hljs-built_in">test <span class="hljs-built_in">test/blocs/user_bloc_test.dart <span class="hljs-comment"># single fileProduction BLoC State
bloc_test verifies state transitions against mocked data. Real production usage involves network latency, server errors, and user actions you didn't anticipate.
HelpMeTest monitors your production services 24/7. When the API your BLoC depends on goes down, you'll know immediately — not from a user complaint. Start free.