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

/test:bloc โ€” ํ™”๋ฉด ์ƒํƒœ ๋ณ€ํ™” ๊ฒ€์‚ฌ๊ธฐ#

ํ•œ๋งˆ๋””๋กœ#

์•ฑ ํ™”๋ฉด์ด "๋กœ๋”ฉ ์ค‘ โ†’ ์™„๋ฃŒ โ†’ ์˜ค๋ฅ˜" ๊ฐ™์€ ์ƒํƒœ๋ฅผ ์˜ฌ๋ฐ”๋ฅธ ์ˆœ์„œ๋กœ ๊ฑฐ์น˜๋Š”์ง€ ์ž๋™์œผ๋กœ ์ ๊ฒ€ํ•ด ์ฃผ๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ์‹ ํ˜ธ๋“ฑ์ด ๋นจ๊ฐ•ยท๋…ธ๋ž‘ยท์ดˆ๋ก์„ ์ •ํ•ด์ง„ ์ˆœ์„œ๋Œ€๋กœ ์ผœ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒ€์‚ฌ์™€ ๋น„์Šทํ•ฉ๋‹ˆ๋‹ค.

๋ˆ„๊ฐ€ยท์–ธ์ œ ์“ฐ๋‚˜์š”#

  • ์•ฑ์˜ ํ™”๋ฉด ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์ฝ”๋“œ(BLoC, Cubit)๋ฅผ ๋งŒ๋“  ๋’ค, ๊ทธ๊ฒƒ์ด ์˜๋„๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์‹ถ์€ ๊ฐœ๋ฐœ์ž
  • /test:bloc ๋ช…๋ น์„ ์‹คํ–‰ํ•  ๋•Œ ์ž๋™์œผ๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. (๋ณ„์นญ: /bloc:test, /test:state)

๋ฌด์—‡์„ ํ•ด์ฃผ๋‚˜์š”#

๋Œ€์ƒ ํ™”๋ฉด ์ฝ”๋“œ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด home_bloc_test.dart, home_cubit_test.dart ๊ฐ™์€ ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜๋ฉฐ, ์ด ํŒŒ์ผ๋“ค์€ ๋‹ค์Œ์„ ์ž๋™์œผ๋กœ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.

  • ์ฒ˜์Œ ์‹œ์ž‘ ์ƒํƒœ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ์ง€
  • ์„ฑ๊ณตํ–ˆ์„ ๋•Œ ์ƒํƒœ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ์ˆœ์„œ๋กœ ๋ฐ”๋€Œ๋Š”์ง€ (์˜ˆ: ๋กœ๋”ฉ โ†’ ์™„๋ฃŒ)
  • ์‹คํŒจํ–ˆ์„ ๋•Œ ์˜ค๋ฅ˜ ์ƒํƒœ๋กœ ์ž˜ ๋„˜์–ด๊ฐ€๋Š”์ง€
  • ๊ฒ€์ƒ‰์ฐฝ์ฒ˜๋Ÿผ ๋น ๋ฅธ ์ž…๋ ฅ์„ ๋ฌถ์–ด์„œ ์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜, ์•Œ๋ฆผ์ฒ˜๋Ÿผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋“ค์–ด์˜ค๋Š” ๋ฐ์ดํ„ฐ๊นŒ์ง€ ์ œ๋Œ€๋กœ ๋ฐ˜์˜ํ•˜๋Š”์ง€

์–ด๋–ป๊ฒŒ ์“ฐ๋‚˜์š”#

# ํŠน์ • ํ™”๋ฉด ์ฝ”๋“œ(BLoC/Cubit)์˜ ํ…Œ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค๋„๋ก ํ˜ธ์ถœ
/test:bloc

ํ•„์š”ํ•˜๋ฉด ๋‹ค์Œ ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ๋„˜๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • target_bloc (ํ•„์ˆ˜) โ€” ๊ฒ€์‚ฌํ•  ํ™”๋ฉด ์ฝ”๋“œ์˜ ์ด๋ฆ„
  • feature_name (์„ ํƒ) โ€” ์–ด๋А ๊ธฐ๋Šฅ ๋ชจ๋“ˆ์ธ์ง€
  • include_cubit (์„ ํƒ) โ€” Cubit๊นŒ์ง€ ํฌํ•จํ• ์ง€ ์—ฌ๋ถ€ (๊ธฐ๋ณธ๊ฐ’: ํฌํ•จ ์•ˆ ํ•จ)

๋งŒ๋“  ๋’ค ์‹ค์ œ๋กœ ๊ฒ€์‚ฌ๋ฅผ ๋Œ๋ฆฌ๋Š” ๋ช…๋ น์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

# ๊ฒ€์‚ฌ์— ํ•„์š”ํ•œ ๋ณด์กฐ ํŒŒ์ผ ๋จผ์ € ์ƒ์„ฑ
dart run build_runner build --delete-conflicting-outputs

# ํ™”๋ฉด ์ƒํƒœ ํ…Œ์ŠคํŠธ๋งŒ ์‹คํ–‰
flutter test test/src/bloc/

์•ˆ์—์„œ ๋ฌด์Šจ ์ผ์ด ๋ฒŒ์–ด์ง€๋‚˜์š”#

  1. ๊ฒ€์‚ฌ์šฉ ๋ณด์กฐ ํŒŒ์ผ(Mock)์„ ๋งŒ๋“ค์–ด, ์‹ค์ œ ์„œ๋ฒ„๋‚˜ ๋ฐ์ดํ„ฐ ์—†์ด๋„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค.
  2. "์ด ์‚ฌ๊ฑด์ด ์ผ์–ด๋‚˜๋ฉด(act) โ†’ ์ด๋Ÿฐ ์ƒํƒœ๊ฐ€ ๋‚˜์™€์•ผ ํ•œ๋‹ค(expect)"๋Š” ์‹์œผ๋กœ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์งญ๋‹ˆ๋‹ค.
  3. ์„ฑ๊ณตยท์‹คํŒจยท์žฌ์‹œ๋„ยท๊ฒ€์ƒ‰ ๋ฌถ์Œ ์ฒ˜๋ฆฌยท์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ฐ™์€ ์—ฌ๋Ÿฌ ์ƒํ™ฉ์„ ๊ฐ๊ฐ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.
  4. ๋งˆ์ง€๋ง‰์œผ๋กœ ์ฒดํฌ๋ฆฌ์ŠคํŠธ(์ฒ˜์Œ ์ƒํƒœ ํ™•์ธ, ์„ฑ๊ณตยท์‹คํŒจ ํ๋ฆ„ ํ™•์ธ ๋“ฑ)๋ฅผ ๋”ฐ๋ผ ๋น ์ง„ ๊ฒ€์‚ฌ๊ฐ€ ์—†๋Š”์ง€ ์ ๊ฒ€ํ•ฉ๋‹ˆ๋‹ค.

โš™๏ธ ์ƒ์„ธ ์˜ต์…˜ยท์‹คํ–‰ ๋ช…์„ธ (๊ฐœ๋ฐœ์ž / AI ์—์ด์ „ํŠธ์šฉ)

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
buildBLoC Instance Generationbuild: () => bloc
seedSet initial stateseed: () => HomeLoaded()
actEmit eventact: (bloc) => bloc.add(event)
expectExpected state Listexpect: () => [State1(), State2()]
verifyAdditional Verificationverify: (_) { verify(...); }
waitAsync wait durationwait: Duration(seconds: 1)
errorsExpected errors Listerrors: () => [isA<Exception>()]
setUpRun before each testsetUp: () async { ... }
tearDownRun after each testtearDown: () 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