0Pricing
Flutter Mobile Development · 강의

bloc_test 및 Mocktail을 활용한 BLoC 테스트

bloc_test의 검증과 모의 의존성을 사용해 결정론적인 단위 테스트를 작성합니다.

bloc_test 및 Mocktail을 활용한 BLoC 테스트은(는) CoddyKit의 무료 Flutter Mobile Development 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Flutter Mobile Development 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why BLoCs Need Deterministic Tests

A BLoC is a pure state machine: it takes events in and emits a predictable sequence of states out. That makes it the most testable layer of a Flutter app.

A good bloc test answers one question: given this starting state and these events, exactly which states get emitted, in what order?

  • Deterministic means the same input always produces the same output sequence.
  • Sources of non-determinism to eliminate: real network calls, DateTime.now(), random values, and timers.

We solve this with two packages: bloc_test for the test harness and mocktail for faking dependencies.

Setting Up the Test Dependencies

Add the test tooling to the dev_dependencies section of pubspec.yaml. None of these ship in your production bundle.

  • bloc_test provides the blocTest helper.
  • mocktail creates mocks with no code generation.
  • test gives you group, setUp, and expect.

Mocktail is preferred over Mockito here because it needs no build_runner step and works cleanly with null safety.

dev_dependencies:
  bloc_test: ^9.1.7
  mocktail: ^1.0.4
  test: ^1.25.0

The BLoC Under Test

Here is the BLoC we will test. A WeatherBloc depends on a WeatherRepository abstraction. On a WeatherRequested event it emits a loading state, then either a success or failure state.

Notice the dependency is injected through the constructor. This is the key design choice that makes the BLoC testable: we can pass in a fake repository instead of one that hits the network.

class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
  WeatherBloc(this._repository) : super(WeatherInitial()) {
    on<WeatherRequested>(_onRequested);
  }

  final WeatherRepository _repository;

  Future<void> _onRequested(
    WeatherRequested event,
    Emitter<WeatherState> emit,
  ) async {
    emit(WeatherLoading());
    try {
      final weather = await _repository.fetch(event.city);
      emit(WeatherSuccess(weather));
    } catch (_) {
      emit(WeatherFailure());
    }
  }
}

Creating a Mock with Mocktail

To isolate the BLoC, we replace the real repository with a mock. With Mocktail you simply extend Mock and implement the abstraction.

  • No annotations, no generated .mocks.dart files.
  • The mock starts with every method stubbed to return null until you tell it otherwise.

You then control its behaviour per test using when(...).thenAnswer(...).

import 'package:mocktail/mocktail.dart';

class MockWeatherRepository extends Mock
    implements WeatherRepository {}

class FakeWeather extends Fake implements Weather {}

Stubbing Return Values

Inside a test, configure the mock before acting. Use thenAnswer for asynchronous methods that return a Future, and thenThrow to simulate an error path.

  • thenAnswer((_) async => value) returns a resolved Future.
  • thenThrow(Exception()) makes the call fail, exercising your catch block.

This is how you force the exact branch you want without any real I/O.

// Success path stub
when(() => repository.fetch('London'))
    .thenAnswer((_) async => const Weather(temp: 14, city: 'London'));

// Failure path stub
when(() => repository.fetch('London'))
    .thenThrow(Exception('network down'));

The blocTest Helper Anatomy

The blocTest function from bloc_test wires up the whole flow. Its core parameters are:

  • build: returns a fresh BLoC instance for each run.
  • act: dispatches one or more events into that BLoC.
  • expect: the ordered list of states that should be emitted, excluding the initial state.

The build callback runs anew every test, guaranteeing a clean slate and no shared state between cases.

blocTest<WeatherBloc, WeatherState>(
  'emits [Loading, Success] when fetch succeeds',
  build: () {
    when(() => repository.fetch(any()))
        .thenAnswer((_) async => const Weather(temp: 14, city: 'London'));
    return WeatherBloc(repository);
  },
  act: (bloc) => bloc.add(const WeatherRequested('London')),
  expect: () => const [
    WeatherLoading(),
    WeatherSuccess(Weather(temp: 14, city: 'London')),
  ],
);

Equatable Makes expect Work

The expect list compares emitted states to your expected states using ==. Plain Dart classes compare by identity, so two different WeatherSuccess instances would never be equal and your test would fail confusingly.

Override value equality with the equatable package. List the fields in props and instances with the same values compare as equal.

abstract class WeatherState extends Equatable {
  const WeatherState();
  @override
  List<Object?> get props => [];
}

class WeatherSuccess extends WeatherState {
  const WeatherSuccess(this.weather);
  final Weather weather;
  @override
  List<Object?> get props => [weather];
}

Testing the Failure Path

Robust tests cover errors, not just the happy path. Stub the dependency to throw, then assert the BLoC emits the failure state instead of crashing.

This proves your try/catch in the handler actually converts exceptions into a user-facing error state.

blocTest<WeatherBloc, WeatherState>(
  'emits [Loading, Failure] when fetch throws',
  build: () {
    when(() => repository.fetch(any()))
        .thenThrow(Exception('network down'));
    return WeatherBloc(repository);
  },
  act: (bloc) => bloc.add(const WeatherRequested('London')),
  expect: () => const [
    WeatherLoading(),
    WeatherFailure(),
  ],
);

registerFallbackValue for any()

Mocktail's any() matcher lets you stub a call regardless of the argument. But for custom types Mocktail cannot construct a placeholder, so it throws at registration time.

Fix this by registering a Fake instance once in setUpAll. Built-in types like String and int never need this; only your own classes do.

setUpAll(() {
  // Required before using any<Weather>() in stubs
  registerFallbackValue(FakeWeather());
});

Verifying Interactions

Beyond asserting emitted states, you often want to confirm the BLoC actually called its dependency the right number of times. Use verify in the verify callback of blocTest, which runs after expect.

  • verify(() => mock.method()).called(1) asserts exactly one call.
  • verifyNever(...) asserts a call never happened.

This catches bugs where the right state is emitted but for the wrong reason.

blocTest<WeatherBloc, WeatherState>(
  'calls repository exactly once',
  build: () {
    when(() => repository.fetch(any()))
        .thenAnswer((_) async => const Weather(temp: 14, city: 'London'));
    return WeatherBloc(repository);
  },
  act: (bloc) => bloc.add(const WeatherRequested('London')),
  verify: (_) {
    verify(() => repository.fetch('London')).called(1);
  },
);

Controlling Time with seed and skip

Two more blocTest parameters give you fine control:

  • seed: provides a starting state, so you can test a transition from any point, not just the initial state.
  • skip: ignores the first N emitted states, useful when you only care about the final one.
  • wait: a Duration to let debounced or delayed events settle before assertions run.

Use seed to test event handlers in isolation without replaying the entire history.

blocTest<CounterBloc, int>(
  'emits [11] from a seeded state of 10',
  build: () => CounterBloc(),
  seed: () => 10,
  act: (bloc) => bloc.add(Increment()),
  expect: () => const [11],
);

Quick Check

A test stubs repository.fetch(any()) where fetch takes a custom Weather argument, and the test crashes at setup with a fallback-value error before any state is emitted. What is the correct fix?

Recap: Deterministic BLoC Testing

You now have a repeatable recipe for testing BLoCs:

  • Inject dependencies through the constructor so they can be swapped for mocks.
  • Create mocks by extending Mock implements with Mocktail; no code generation needed.
  • Stub behaviour with thenAnswer for success and thenThrow for failure paths.
  • Use blocTest with build / act / expect to assert the exact ordered states.
  • Make expect reliable with Equatable value equality.
  • Register fallbacks with registerFallbackValue when matching custom types via any().
  • Confirm intent with verify, and control flow with seed, skip, and wait.

Cover both the happy and failure paths, and your BLoC layer becomes fully deterministic and regression-proof.

자주 묻는 질문

“bloc_test 및 Mocktail을 활용한 BLoC 테스트” 강의는 무료인가요?

네 — “bloc_test 및 Mocktail을 활용한 BLoC 테스트” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Flutter Mobile Development 강의 전체를 잠금 해제할 수 있습니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.

“bloc_test 및 Mocktail을 활용한 BLoC 테스트”에서 뭘 배우나요?

bloc_test의 검증과 모의 의존성을 사용해 결정론적인 단위 테스트를 작성합니다. 브라우저에서 직접 실행하는 실습 코드로 Flutter Mobile Development을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Flutter Mobile Development을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Flutter Mobile Development은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“bloc_test 및 Mocktail을 활용한 BLoC 테스트” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Flutter Mobile Development 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Flutter Mobile Development 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 이벤트, 상태 및 Cubit과 Bloc 중 선택하기
  2. BLoC의 스트림 변환기 및 이벤트 디바운싱
  3. HydratedBloc을 활용한 상태 저장
  4. bloc_test 및 Mocktail을 활용한 BLoC 테스트
← Flutter Mobile Development(으)로 돌아가기