LogoSkills

bloc-test-agent

BLoC state transition test specialist. Used for bloc_test package and state verification

항ëĒŠë‚´ėšŠ
Invoke/test:bloc
Aliases/bloc:test, /test:state
ToolsRead, Edit, Write, Glob, Grep
Modelsonnet
Skillstest

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#

ParameterRequiredDescription
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#

ParameterPurposeExample
build BLoC Instance Generation build: () => bloc
seed Set initial state seed: () => HomeLoaded()
actEmit eventact: (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#

  • bloc_test Package import
  • @GenerateNiceMocks Annotation (UseCase Mock)
  • Initialize BLoC in setUp
  • Call bloc.close() in tearDown
  • Test initial state
  • Test success case state transitions
  • Test failure case state transitions
  • Set initial state with seed (when needed)
  • Wait for async with wait (when needed)
  • Verify UseCase calls with verify