íëĒŠ ë´ėŠ
Invoke /test:bloc
Aliases /bloc:test, /test:state
Tools Read, Edit, Write, Glob, Grep
Model sonnet
Skills test
BLoC Test Agent #
Specialized agent for BLoC state transition testing
Role #
Tests BLoC state transitions.
Uses bloc_test package
build, act, expect Pattern
State transition verification
Event processing verification
Activation Conditions #
/test:bloc Activated when command is invoked
Invoked when writing BLoC, Cubit state tests
Parameters #
Parameter Required Description
target_blocâ
Test target BLoC/Cubit Class name
feature_nameâ Feature module name
include_cubitâ Cubit Includes Whether (default: false)
Test File Structure #
feature/ { module_type} / { feature_name} / test/
âââ src/
â âââ bloc/
â â âââ { feature} _bloc_test . dart
â â âââ { feature} _cubit_test . dart
â âââ fixture/
â âââ { feature} _fixture . dart
âââ { feature} _test . dart # Test entry point
Import Order (Required) #
// 1. Dart test
import 'package:bloc_test/bloc_test.dart' ;
import 'package:flutter_test/flutter_test.dart' ;
// 2. Mock package
import 'package:mockito/annotations.dart' ;
import 'package:mockito/mockito.dart' ;
// 3. Dependency packages
import 'package:dependencies/dependencies.dart' ;
// 4. Test target
import 'package:{feature}/src/presentation/bloc/{feature}_bloc.dart' ;
import 'package:{feature}/src/domain/usecase/get_{entity}_usecase.dart' ;
// 5. Generated files
import '{feature}_bloc_test.mocks.dart' ;
Core Patterns #
1. BLoC Test Default Structure #
import 'package:bloc_test/bloc_test.dart' ;
import 'package:dependencies/dependencies.dart' ;
import 'package:feature_home/src/domain/entity/user.dart' ;
import 'package:feature_home/src/domain/usecase/get_user_usecase.dart' ;
import 'package:feature_home/src/presentation/bloc/home_bloc.dart' ;
import 'package:flutter_test/flutter_test.dart' ;
import 'package:mockito/annotations.dart' ;
import 'package:mockito/mockito.dart' ;
import 'home_bloc_test.mocks.dart' ;
@GenerateNiceMocks ( [ MockSpec < GetUserUseCase > ( ) ] )
void main ( ) {
late HomeBloC bloc;
late MockGetUserUseCase mockGetUserUseCase;
setUp ( ( ) {
mockGetUserUseCase = MockGetUserUseCase ( ) ;
bloc = HomeBloC ( mockGetUserUseCase) ;
} ) ;
tearDown ( ( ) {
bloc. close ( ) ;
} ) ;
group ( 'HomeBloC' , ( ) {
const tUser = User ( id: 1 , name: 'í길ë' , email: 'hong@example.com' ) ;
test ( 'initial state should be HomeInitial' , ( ) {
expect ( bloc. state, equals ( const HomeInitial ( ) ) ) ;
} ) ;
blocTest < HomeBloC , HomeState > (
'emits [HomeLoading, HomeLoaded] when LoadUser is added' ,
build: ( ) {
when ( mockGetUserUseCase ( any) )
. thenAnswer ( ( _) async => const Right ( tUser) ) ;
return bloc;
} ,
act: ( bloc) => bloc. add ( const HomeEvent . loadUser ( id: 1 ) ) ,
expect: ( ) => [
const HomeLoading ( ) ,
const HomeLoaded ( user: tUser) ,
] ,
verify: ( _) {
verify ( mockGetUserUseCase ( const GetUserParams ( id: 1 ) ) ) . called ( 1 ) ;
} ,
) ;
blocTest < HomeBloC , HomeState > (
'emits [HomeLoading, HomeError] when LoadUser fails' ,
build: ( ) {
when ( mockGetUserUseCase ( any) )
. thenAnswer ( ( _) async => const Left ( ServerFailure ( message: 'ėë˛ ė¤ëĨ' ) ) ) ;
return bloc;
} ,
act: ( bloc) => bloc. add ( const HomeEvent . loadUser ( id: 1 ) ) ,
expect: ( ) => [
const HomeLoading ( ) ,
isA < HomeError > ( ) . having (
( s) => s. failure. message,
'failure message' ,
'ėë˛ ė¤ëĨ' ,
) ,
] ,
) ;
} ) ;
}
2. Cubit Test #
import 'package:bloc_test/bloc_test.dart' ;
import 'package:feature_counter/src/presentation/cubit/counter_cubit.dart' ;
import 'package:flutter_test/flutter_test.dart' ;
void main ( ) {
late CounterCubit cubit;
setUp ( ( ) {
cubit = CounterCubit ( ) ;
} ) ;
tearDown ( ( ) {
cubit. close ( ) ;
} ) ;
group ( 'CounterCubit' , ( ) {
test ( 'initial state should be 0' , ( ) {
expect ( cubit. state, equals ( 0 ) ) ;
} ) ;
blocTest < CounterCubit , int > (
'emits [1] when increment is called' ,
build: ( ) => cubit,
act: ( cubit) => cubit. increment ( ) ,
expect: ( ) => [ 1 ] ,
) ;
blocTest < CounterCubit , int > (
'emits [-1] when decrement is called' ,
build: ( ) => cubit,
act: ( cubit) => cubit. decrement ( ) ,
expect: ( ) => [ - 1 ] ,
) ;
blocTest < CounterCubit , int > (
'emits [1, 2, 3] when increment is called 3 times' ,
build: ( ) => cubit,
act: ( cubit) {
cubit . increment ( ) ;
cubit. increment ( ) ;
cubit. increment ( ) ;
} ,
expect: ( ) => [ 1 , 2 , 3 ] ,
) ;
} ) ;
}
3. Complex State Transition Test #
blocTest < HomeBloC , HomeState > (
'emits correct states for pagination flow' ,
build: ( ) {
when ( mockGetUsersUseCase ( any) ) . thenAnswer ( ( _) async => Right ( users) ) ;
return bloc;
} ,
seed: ( ) => const HomeLoaded ( users: [ ] , hasMore: true , page: 1 ) ,
act: ( bloc) => bloc. add ( const HomeEvent . loadMore ( ) ) ,
expect: ( ) => [
// ëĄëŠ ėí
const HomeLoaded ( users: [ ] , hasMore: true , page: 1 , isLoadingMore: true ) ,
// ëĄë ėëŖ ėí
HomeLoaded ( users: users, hasMore: false , page: 2 , isLoadingMore: false ) ,
] ,
) ;
blocTest < HomeBloC , HomeState > (
'does not emit new state when already loading' ,
build: ( ) => bloc,
seed: ( ) => const HomeLoading ( ) ,
act: ( bloc) => bloc. add ( const HomeEvent . loadUser ( id: 1 ) ) ,
expect: ( ) => [ ] ,
verify: ( _) {
verifyNever ( mockGetUserUseCase ( any) ) ;
} ,
) ;
4. Error Recovery Test #
blocTest < HomeBloC , HomeState > (
'can retry after error' ,
build: ( ) {
var callCount = 0 ;
when ( mockGetUserUseCase ( any) ) . thenAnswer ( ( _) async {
callCount ++ ;
if ( callCount == 1 ) {
return const Left ( NetworkFailure ( message: 'ë¤í¸ėíŦ ė¤ëĨ' ) ) ;
}
return const Right ( tUser) ;
} ) ;
return bloc;
} ,
act: ( bloc) async {
bloc . add ( const HomeEvent . loadUser ( id: 1 ) ) ;
await Future . delayed ( const Duration ( milliseconds: 100 ) ) ;
bloc. add ( const HomeEvent . retry ( ) ) ;
} ,
expect: ( ) => [
const HomeLoading ( ) ,
isA < HomeError > ( ) ,
const HomeLoading ( ) ,
const HomeLoaded ( user: tUser) ,
] ,
) ;
5. Debounce/Throttle Test #
blocTest < SearchBloC , SearchState > (
'debounces search queries' ,
build: ( ) {
when ( mockSearchUseCase ( any) )
. thenAnswer ( ( _) async => const Right ( searchResults) ) ;
return SearchBloC ( mockSearchUseCase) ;
} ,
act: ( bloc) async {
bloc . add ( const SearchEvent . queryChanged ( 'a' ) ) ;
bloc. add ( const SearchEvent . queryChanged ( 'ab' ) ) ;
bloc. add ( const SearchEvent . queryChanged ( 'abc' ) ) ;
await Future . delayed ( const Duration ( milliseconds: 500 ) ) ;
} ,
wait: const Duration ( milliseconds: 600 ) ,
expect: ( ) => [
const SearchLoading ( ) ,
const SearchLoaded ( results: searchResults) ,
] ,
verify: ( _) {
// ëë°ė´ė¤ëĄ ė¸í´ ë§ė§ë§ ėŋŧëĻŦë§ ė¤íë¨
verify ( mockSearchUseCase ( const SearchParams ( query: 'abc' ) ) ) . called ( 1 ) ;
verifyNever ( mockSearchUseCase ( const SearchParams ( query: 'a' ) ) ) ;
verifyNever ( mockSearchUseCase ( const SearchParams ( query: 'ab' ) ) ) ;
} ,
) ;
6. Stream Subscription Test #
blocTest < NotificationBloC , NotificationState > (
'updates state when stream emits new data' ,
build: ( ) {
final controller = StreamController < List < Notification >> ( ) ;
when ( mockWatchNotificationsUseCase ( ) )
. thenAnswer ( ( _) => controller. stream) ;
// í
ė¤í¸ in progress ė¤í¸ëĻŧė ë°ė´í° ėļę°
Future . delayed ( const Duration ( milliseconds: 50 ) , ( ) {
controller . add ( [ notification1] ) ;
} ) ;
Future . delayed ( const Duration ( milliseconds: 100 ) , ( ) {
controller . add ( [ notification1, notification2] ) ;
} ) ;
return NotificationBloC ( mockWatchNotificationsUseCase) ;
} ,
act: ( bloc) => bloc. add ( const NotificationEvent . startWatching ( ) ) ,
wait: const Duration ( milliseconds: 200 ) ,
expect: ( ) => [
const NotificationLoading ( ) ,
NotificationLoaded ( notifications: [ notification1] ) ,
NotificationLoaded ( notifications: [ notification1 , notification2] ) ,
] ,
) ;
7. Sealed Class State Matching #
blocTest < HomeBloC , HomeState > (
'emits correct state with pattern matching verification' ,
build: ( ) {
when ( mockGetUserUseCase ( any) )
. thenAnswer ( ( _) async => const Right ( tUser) ) ;
return bloc;
} ,
act: ( bloc) => bloc. add ( const HomeEvent . loadUser ( id: 1 ) ) ,
verify: ( bloc) {
final state = bloc. state;
switch ( state) {
case HomeLoaded ( : final user) :
expect ( user, equals ( tUser) ) ;
case HomeError ( ) :
fail ( 'Expected HomeLoaded but got HomeError' ) ;
case HomeLoading ( ) :
fail ( 'Expected HomeLoaded but got HomeLoading' ) ;
case HomeInitial ( ) :
fail ( 'Expected HomeLoaded but got HomeInitial' ) ;
}
} ,
) ;
blocTest Parameter Summary #
Parameter Purpose Example
build
BLoC Instance Generation
build: () => bloc
seed
Set initial state
seed: () => HomeLoaded()
actEmit event act: (bloc) => bloc.add(event)
expect
Expected state List
expect: () => [State1(), State2()]
verify
Additional Verification
verify: (_) { verify(...); }
wait
Async wait duration
wait: Duration(seconds: 1)
errors
Expected errors List
errors: () => [isA<Exception>()]
setUp
Run before each test
setUp: () async { ... }
tearDown
Run after each test
tearDown: () async { ... }
State Matcher Patterns #
// íė
ę˛ėĻ
expect: ( ) => [ isA < HomeLoading > ( ) , isA < HomeLoaded > ( ) ] ,
// ėėą ę˛ėĻ
expect: ( ) => [
isA < HomeLoaded > ( )
. having ( ( s) => s. user. id, 'user id' , 1 )
. having ( ( s) => s. user. name, 'user name' , 'í길ë' ) ,
] ,
// ėŦëŦ ėėą ę˛ėĻ
expect: ( ) => [
isA < HomeError > ( )
. having ( ( s) => s. failure, 'failure' , isA < ServerFailure > ( ) )
. having ( ( s) => s. failure. message, 'message' , contains ( 'ėë˛' ) ) ,
] ,
// ė íí ę° ę˛ėĻ
expect: ( ) => [
const HomeLoading ( ) ,
const HomeLoaded ( user: tUser) ,
] ,
Build Commands #
# Mock ėėą
cd feature/ { module_type} / { feature_name}
dart run build_runner build -- delete- conflicting- outputs
# BLoC í
ė¤í¸ë§ ė¤í
flutter test test/ src/ bloc/
# íšė BLoC í
ė¤í¸ ė¤í
flutter test test/ src/ bloc/ home_bloc_test. dart
# Test with coverage
melos run test: with - html- coverage
Reference Files #
feature/ application/ home/ test/ src/ bloc/ home_bloc_test. dart
feature/ application/ store/ test/ src/ bloc/ store_bloc_test. dart
feature/ console/ instructor/ test/ src/ bloc/ instructor_bloc_test. dart
Checklist #