flutter
/

BLoC Unit Testing: Complete Guide to Testing BLoCs & Cubits

Last Sync: Today

On this page

13
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

BLoC Unit Testing: Complete Guide to Testing BLoCs & Cubits

What is BLoC Unit Testing?

BLoC unit testing involves writing automated tests for your BLoC and Cubit classes to verify that they correctly respond to events, manage state transitions, and handle errors. The bloc_test package provides a dedicated test helper that simplifies testing by reducing boilerplate and making state assertions straightforward. Proper testing ensures your business logic remains reliable as your app grows.

Why Test BLoCs?

  • Catch Bugs Early – Verify state changes and edge cases before shipping.
  • Refactor with Confidence – Tests act as a safety net when you change internal logic.
  • Document Expected Behavior – Tests serve as living documentation.
  • Improve Code Quality – Forces you to write testable, modular code.
  • Faster Development – Reduces manual testing effort over time.

Setting Up Testing Environment

Add the required dev dependencies to your pubspec.yaml:

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

Testing a Cubit

Cubits are simpler – they have synchronous or asynchronous methods that emit states. Use blocTest to test them easily.

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

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

BLoCs use events; blocTest allows you to add multiple events and verify the emitted states.

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

void main() {
  group('CounterBloc', () {
    blocTest<CounterBloc, CounterState>(
      'emits [Loading, Loaded] when LoadCounter event is added',
      build: () => CounterBloc(repository: MockCounterRepository()),
      act: (bloc) => bloc.add(LoadCounter()),
      expect: () => [
        CounterLoading(),
        CounterLoaded(42), // assuming repository returns 42
      ],
    );
  });
}

Mocking Dependencies with Mocktail

Use mocktail to mock repositories or services. Create a mock class, set up stubs, and inject into your BLoC.

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

class MockCounterRepository extends Mock implements CounterRepository {}

void main() {
  late MockCounterRepository mockRepository;

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

  group('CounterBloc', () {
    blocTest<CounterBloc, CounterState>(
      'emits [Loading, Loaded] when LoadCounter succeeds',
      setUp: () {
        when(() => mockRepository.fetchCount())
            .thenAnswer((_) async => 42);
      },
      build: () => CounterBloc(repository: mockRepository),
      act: (bloc) => bloc.add(LoadCounter()),
      expect: () => [
        CounterLoading(),
        CounterLoaded(42),
      ],
    );

    blocTest<CounterBloc, CounterState>(
      'emits [Loading, Error] when LoadCounter fails',
      setUp: () {
        when(() => mockRepository.fetchCount())
            .thenThrow(Exception('Network error'));
      },
      build: () => CounterBloc(repository: mockRepository),
      act: (bloc) => bloc.add(LoadCounter()),
      expect: () => [
        CounterLoading(),
        isA<CounterError>(),
      ],
    );
  });
}

Testing Asynchronous Operations

Use await in act and wait parameter to handle delays. blocTest supports asynchronous act functions.

DARTRead-only
1
blocTest<AsyncBloc, AsyncState>(
  'emits [Loading, Loaded] after delay',
  build: () => AsyncBloc(),
  act: (bloc) async => bloc.add(LoadData()),
  wait: const Duration(milliseconds: 500),
  expect: () => [
    AsyncLoading(),
    AsyncLoaded('data'),
  ],
);

Testing State Equality

Always extend Equatable in your states and events to enable proper equality checks. This ensures blocTest correctly compares expected and actual states.

DARTRead-only
1
class CounterState extends Equatable {
  final int value;
  const CounterState(this.value);
  @override
  List<Object?> get props => [value];
}

// Without Equatable, you'd have to manually implement == and hashCode.

Testing Event Transformers

To test debounce or throttle, you may need to control time. Use fake_async package or simulate events with delays. For example, testing debounce:

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

test('debounce only processes the last event', () {
  fakeAsync((async) {
    final bloc = SearchBloc();
    bloc.add(SearchQueryChanged('a'));
    bloc.add(SearchQueryChanged('ab'));
    async.elapse(const Duration(milliseconds: 300));
    // Only one state change expected after debounce
    expect(bloc.state, SearchLoading()); // etc.
    async.flushMicrotasks();
  });
});

Testing BLoC Lifecycle

You can also test that onClose cleans up resources, but it's often sufficient to test that the BLoC does not leak by verifying no errors after closing.

DARTRead-only
1
test('closes stream without error', () {
  final bloc = CounterBloc();
  bloc.close();
  // No exception
});

Best Practices

  • Use blocTest – It reduces boilerplate and provides a clean API for state verification.
  • Mock dependencies – Isolate the BLoC from real repositories using mocktail or mockito.
  • Test happy paths and error paths – Ensure both success and failure scenarios are covered.
  • Use isA matcher – When exact state details are not important, use isA<StateType>() to verify the type.
  • Group tests logically – Use group to organize related tests.
  • Keep tests fast – Avoid real network calls or database operations.
  • Test state transitions – Verify the sequence of states emitted, not just the final one.
  • Test with blocTest’s seed – Provide initial state if needed.

Common Mistakes

  • ❌ Not using Equatable – State comparisons fail, causing false positives.
  • ❌ Forgetting to mock dependencies – Real network calls make tests flaky and slow.
  • ❌ Not waiting for async operations – Use wait or await to let async work complete.
  • ❌ Testing implementation details – Focus on state outputs, not internal method calls.
  • ❌ Ignoring event transformers – Test that debounce/throttle works as expected (use fake_async).
  • ❌ Not cleaning up – Always close the bloc in tests, or blocTest does it automatically.

Conclusion

Unit testing BLoCs and Cubits is essential for building reliable Flutter applications. With the bloc_test package and mocktail, you can write concise, readable tests that validate state transitions, error handling, and asynchronous behavior. By following best practices and avoiding common pitfalls, you’ll gain confidence in your state management code and reduce regression bugs.

Try it yourself

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

// This example shows a simple CounterBloc and includes test examples in comments.
// To run tests, you would normally use flutter test with bloc_test.

// -------------------- BLoC Implementation --------------------
// States
class CounterState extends Equatable {
  final int value;
  final bool isLoading;

  const CounterState({this.value = 0, this.isLoading = false});

  CounterState copyWith({int? value, bool? isLoading}) {
    return CounterState(
      value: value ?? this.value,
      isLoading: isLoading ?? this.isLoading,
    );
  }

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

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

class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
class Reset extends CounterEvent {}
class LoadCounter extends CounterEvent {}

// Repository (mock in tests)
class CounterRepository {
  Future<int> fetchCount() async {
    // Simulate network delay
    await Future.delayed(Duration(milliseconds: 300));
    return 42;
  }
}

// BLoC
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  final CounterRepository repository;

  CounterBloc({required this.repository}) : super(const CounterState()) {
    on<Increment>((event, emit) {
      emit(state.copyWith(value: state.value + 1));
    });
    on<Decrement>((event, emit) {
      emit(state.copyWith(value: state.value - 1));
    });
    on<Reset>((event, emit) {
      emit(const CounterState());
    });
    on<LoadCounter>(_onLoadCounter);
  }

  Future<void> _onLoadCounter(LoadCounter event, Emitter<CounterState> emit) async {
    emit(state.copyWith(isLoading: true));
    try {
      final value = await repository.fetchCount();
      emit(state.copyWith(value: value, isLoading: false));
    } catch (e) {
      emit(state.copyWith(isLoading: false));
      // You might also emit an error state
    }
  }
}

// -------------------- UI --------------------
void main() => runApp(MyApp());

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

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter BLoC')),
      body: BlocBuilder<CounterBloc, CounterState>(
        builder: (context, state) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                if (state.isLoading)
                  CircularProgressIndicator()
                else
                  Text('Value: ${state.value}', style: TextStyle(fontSize: 32)),
                SizedBox(height: 20),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    ElevatedButton(
                      onPressed: () => context.read<CounterBloc>().add(Increment()),
                      child: Icon(Icons.add),
                    ),
                    SizedBox(width: 20),
                    ElevatedButton(
                      onPressed: () => context.read<CounterBloc>().add(Decrement()),
                      child: Icon(Icons.remove),
                    ),
                    SizedBox(width: 20),
                    ElevatedButton(
                      onPressed: () => context.read<CounterBloc>().add(Reset()),
                      child: Text('Reset'),
                    ),
                    SizedBox(width: 20),
                    ElevatedButton(
                      onPressed: () => context.read<CounterBloc>().add(LoadCounter()),
                      child: Text('Load'),
                    ),
                  ],
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

/*
  -------------------- UNIT TESTS EXAMPLE --------------------
  
  // Add to test/counter_bloc_test.dart:
  import 'package:bloc_test/bloc_test.dart';
  import 'package:flutter_test/flutter_test.dart';
  import 'package:mocktail/mocktail.dart';
  import 'package:my_app/counter_bloc.dart';

  class MockCounterRepository extends Mock implements CounterRepository {}

  void main() {
    group('CounterBloc', () {
      late MockCounterRepository mockRepository;

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

      blocTest<CounterBloc, CounterState>(
        'emits state with incremented value',
        build: () => CounterBloc(repository: mockRepository),
        act: (bloc) => bloc.add(Increment()),
        expect: () => [
          CounterState(value: 1),
        ],
      );

      blocTest<CounterBloc, CounterState>(
        'emits state with decremented value',
        build: () => CounterBloc(repository: mockRepository),
        act: (bloc) => bloc.add(Decrement()),
        expect: () => [
          CounterState(value: -1),
        ],
      );

      blocTest<CounterBloc, CounterState>(
        'emits loading and loaded states when LoadCounter succeeds',
        setUp: () {
          when(() => mockRepository.fetchCount()).thenAnswer((_) async => 42);
        },
        build: () => CounterBloc(repository: mockRepository),
        act: (bloc) => bloc.add(LoadCounter()),
        wait: const Duration(milliseconds: 500),
        expect: () => [
          CounterState(isLoading: true),
          CounterState(value: 42, isLoading: false),
        ],
      );

      blocTest<CounterBloc, CounterState>(
        'handles LoadCounter error gracefully',
        setUp: () {
          when(() => mockRepository.fetchCount()).thenThrow(Exception('Network error'));
        },
        build: () => CounterBloc(repository: mockRepository),
        act: (bloc) => bloc.add(LoadCounter()),
        wait: const Duration(milliseconds: 500),
        expect: () => [
          CounterState(isLoading: true),
          CounterState(isLoading: false),
        ],
      );
    });
  }
*/

Test Your Knowledge

Q1
of 4

Which package provides the `blocTest` helper function?

A
flutter_test
B
mocktail
C
bloc_test
D
bloc_testing
Q2
of 4

What should you extend for your state classes to enable correct equality checks in tests?

A
Comparable
B
Equatable
C
Object
D
BlocBase
Q3
of 4

How do you simulate a delayed async operation in a test with `blocTest`?

A
Use `Future.delayed` in `act`
B
Use the `wait` parameter
C
Use `setTimeout`
D
It's automatic
Q4
of 4

What is the recommended way to mock dependencies in BLoC tests?

A
Use real implementations
B
Use mocktail or mockito
C
Create fake classes manually
D
Use build_runner

Frequently Asked Questions

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

blocTest handles the setup, event addition, state listening, and teardown automatically. It also provides convenient parameters like act, expect, wait, and skip. Manual testing requires more code and careful stream handling.

Can I use `blocTest` with Cubit?

Yes, blocTest works with any BlocBase (both BLoC and Cubit). Just specify the correct type parameters.

How do I test a BLoC that uses `HydratedBloc`?

You can mock the storage or use a temporary directory. For unit tests, it's easier to use HydratedBloc.storage = null and rely on in-memory state. For integration, you may need to set up a test storage directory.

How do I test events that trigger multiple state changes?

Simply list all expected states in the expect parameter in order. blocTest will compare the entire sequence.

Should I test every possible event combination?

Focus on critical paths and edge cases. Full coverage is ideal but prioritize scenarios that are likely to break or are complex.

Previous

bloc testing

Next

bloc mocktail

Related Content

Need help?

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