flutter
/

BLoC Testing: Write Robust Tests for Your BLoCs & Cubits

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

BLoC Testing: Write Robust Tests for Your BLoCs & Cubits

Introduction

Testing is a critical part of building reliable Flutter applications. With the BLoC pattern, you can easily test your business logic in isolation without dealing with UI. This guide covers everything you need to know about testing BLoCs and Cubits – from simple unit tests to advanced scenarios using blocTest, mocking dependencies, and testing error handling.

Why Test BLoCs?

  • Isolated Logic – BLoCs contain pure business logic, making them ideal for unit tests.
  • Predictable State Transitions – Given an event and initial state, the emitted states should be deterministic.
  • Catch Bugs Early – Ensure your state changes behave as expected before they reach the UI.
  • Refactor with Confidence – Tests give you safety when changing internal implementations.
  • Documentation – Tests serve as living documentation of how your BLoC should behave.

Setting Up Testing Dependencies

Add the required packages to your pubspec.yaml dev dependencies:

YAMLRead-only
1
dev_dependencies:
  flutter_test:
    sdk: flutter
  bloc_test: ^9.1.5
  mocktail: ^1.0.0
  equatable: ^2.0.5

Testing a Simple Cubit

Let's start with a simple counter Cubit. The blocTest function from bloc_test is the main tool for testing BLoCs and Cubits.

DARTRead-only
1
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}

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] when decrement is called',
      build: () => CounterCubit(),
      act: (cubit) => cubit.decrement(),
      expect: () => [-1],
    );
  });
}

Testing a Bloc with Events

For BLoCs that use events, the pattern is similar. You provide events to act and expected states to expect.

DARTRead-only
1
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
    on<Decrement>((event, emit) => emit(state - 1));
  }
}

void main() {
  group('CounterBloc', () {
    blocTest<CounterBloc, int>(
      'emits [1] when Increment is added',
      build: () => CounterBloc(),
      act: (bloc) => bloc.add(Increment()),
      expect: () => [1],
    );
  });
}

Testing with Dependencies (Mocking)

Most real‑world BLoCs depend on repositories or services. Use mocktail to mock dependencies and control their behavior. The build function can create the BLoC with mocked dependencies.

DARTRead-only
1
class UserRepository {
  Future<User> getUser(String id) async { ... }
}

class UserBloc extends Bloc<UserEvent, UserState> {
  final UserRepository repository;

  UserBloc(this.repository) : super(UserInitial()) {
    on<LoadUser>(_onLoadUser);
  }

  Future<void> _onLoadUser(LoadUser event, Emitter<UserState> emit) async {
    emit(UserLoading());
    try {
      final user = await repository.getUser(event.id);
      emit(UserLoaded(user));
    } catch (e) {
      emit(UserError(e.toString()));
    }
  }
}

// Test
import 'package:mocktail/mocktail.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  late MockUserRepository mockRepository;

  setUp(() {
    mockRepository = MockUserRepository();
  });

  group('UserBloc', () {
    blocTest<UserBloc, UserState>(
      'emits [UserLoading, UserLoaded] when user is found',
      build: () => UserBloc(mockRepository),
      setUp: () {
        when(() => mockRepository.getUser('123'))
            .thenAnswer((_) async => User(id: '123', name: 'John'));
      },
      act: (bloc) => bloc.add(LoadUser('123')),
      expect: () => [
        UserLoading(),
        UserLoaded(User(id: '123', name: 'John')),
      ],
    );

    blocTest<UserBloc, UserState>(
      'emits [UserLoading, UserError] when repository throws',
      build: () => UserBloc(mockRepository),
      setUp: () {
        when(() => mockRepository.getUser('123'))
            .thenThrow(Exception('Not found'));
      },
      act: (bloc) => bloc.add(LoadUser('123')),
      expect: () => [
        UserLoading(),
        UserError('Exception: Not found'),
      ],
    );
  });
}

Testing State Equality with Equatable

Use Equatable in your state classes so that blocTest can correctly compare expected and actual states. Without proper equality, tests may fail even if the data is the same.

DARTRead-only
1
@immutable
class UserState extends Equatable {
  final User? user;
  final String? error;
  final bool isLoading;

  const UserState({this.user, this.error, this.isLoading = false});

  @override
  List<Object?> get props => [user, error, isLoading];
}

Testing with Streams and emit.forEach

If your BLoC uses emit.forEach, you can test it by providing a controlled stream. Use StreamController to emit values during the test.

DARTRead-only
1
class TimerBloc extends Bloc<TimerEvent, TimerState> {
  TimerBloc() : super(TimerInitial()) {
    on<TimerStarted>(_onTimerStarted);
  }

  Future<void> _onTimerStarted(TimerStarted event, Emitter<TimerState> emit) async {
    await emit.forEach(
      _tickStream(event.duration),
      onData: (duration) => TimerRunInProgress(duration),
    );
    emit(TimerRunComplete());
  }

  Stream<int> _tickStream(int duration) async* {
    for (int i = duration; i >= 0; i--) {
      await Future.delayed(Duration(seconds: 1));
      yield i;
    }
  }
}

// Test
blocTest<TimerBloc, TimerState>(
  'emits progress and complete',
  build: () => TimerBloc(),
  act: (bloc) => bloc.add(TimerStarted(2)),
  expect: () => [
    TimerRunInProgress(2),
    TimerRunInProgress(1),
    TimerRunInProgress(0),
    TimerRunComplete(),
  ],
);

Testing BlocObserver

You can test custom BlocObserver by creating a test observer and verifying that methods are called.

DARTRead-only
1
class TestObserver extends BlocObserver {
  List<BlocEvent> events = [];
  List<Transition> transitions = [];

  @override
  void onEvent(Bloc bloc, Object? event) {
    events.add(BlocEvent(bloc.runtimeType, event));
    super.onEvent(bloc, event);
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    transitions.add(transition);
    super.onTransition(bloc, transition);
  }
}

void main() {
  test('observer records events and transitions', () {
    final observer = TestObserver();
    Bloc.observer = observer;

    final bloc = CounterBloc();
    bloc.add(Increment());

    expect(observer.events.length, 1);
    expect(observer.transitions.length, 1);
  });
}

Best Practices

  • Use blocTest – It provides a clean, declarative API for testing state emissions.
  • Mock dependencies with mocktail – Avoid using real network/database calls in unit tests.
  • Use Equatable in states – Simplifies equality comparisons in tests.
  • Test error cases – Ensure your BLoC handles exceptions and emits error states.
  • Keep tests focused – Test one scenario per blocTest.
  • Use setUp and tearDown – Reset mocks and observers between tests.
  • Avoid async in act – blocTest handles async automatically; just add events.
  • Use skip to ignore initial state – If you don't want to verify the starting state.

Common Mistakes

  • ❌ Not mocking dependencies – Tests become slow and unreliable. ✅ Always mock external services.
  • ❌ Using expect inside act – Causes race conditions; use the expect parameter of blocTest.
  • ❌ Forgetting to register adapters for Hive – Tests will fail when using real Hive. ✅ Use in‑memory boxes or mock Hive.
  • ❌ Testing UI together with BLoC – Should be separate; BLoC tests should be pure Dart tests (no WidgetTester).
  • ❌ Ignoring state equality – Leads to false negatives. ✅ Implement == or use Equatable.

Conclusion

Testing BLoCs is straightforward with the bloc_test package. By isolating business logic, mocking dependencies, and using blocTest, you can write comprehensive tests that verify state transitions, error handling, and edge cases. A well‑tested BLoC layer gives you confidence to refactor and add features without breaking existing functionality.

Try it yourself

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() => runApp(MyApp());

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  void increment() => emit(state + 1);
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => CounterCubit(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final cubit = context.watch<CounterCubit>();
    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Count:', style: TextStyle(fontSize: 20)),
            Text('${cubit.state}', style: TextStyle(fontSize: 40)),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: cubit.increment,
              child: Text('Increment'),
            ),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

Which function is the primary tool for testing BLoCs in the bloc_test package?

A
testBloc
B
blocTest
C
BlocTester
D
testState
Q2
of 3

How do you mock dependencies in BLoC tests?

A
Use real implementations
B
Use mocktail or Mockito
C
Use setState
D
Use Future.delayed
Q3
of 3

Why is Equatable recommended for BLoC states?

A
It improves performance
B
It simplifies state equality comparison
C
It adds encryption
D
It provides automatic JSON serialization

Frequently Asked Questions

What is the difference between `blocTest` and manually testing with `Stream`?

blocTest handles subscription management, waiting for emissions, and timeouts automatically. It reduces boilerplate and is the recommended way to test BLoCs.

Can I test BLoCs that use `emit.forEach`?

Yes, blocTest works with any BLoC that emits states over time. The test will wait until all expected states are emitted or until a timeout.

How do I test a BLoC that uses `debounceTime` or other stream transformers?

You can use blocTest with skip to ignore intermediate states, or use wait to let the debounce complete. Alternatively, mock the time with FakeAsync for precise control.

Should I test BLoCs in integration tests?

Unit tests are sufficient for BLoC logic. Integration tests should verify that UI interacts correctly with the BLoC, but the BLoC itself can be unit tested separately.

How do I test a Cubit that has asynchronous operations?

blocTest works the same for Cubits. Use act to call the asynchronous method and expect to verify the emitted states.

Previous

bloc local storage

Next

bloc unit testing

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.