flutter
/

BLoC Widget Testing: Testing Flutter UI with BLoC State Management

Last Sync: Today

On this page

11
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

BLoC Widget Testing: Testing Flutter UI with BLoC State Management

What is BLoC Widget Testing?

BLoC widget testing refers to testing Flutter widgets that rely on BLoC for state management. Unlike unit tests that test BLoCs in isolation, widget tests verify the integration between the UI and the BLoC. You ensure that the widget correctly displays states, responds to user interactions, and updates when the BLoC emits new states. This level of testing is crucial for building reliable, production-ready apps.

Why Test Widgets with BLoC?

  • Verify UI Behavior – Ensure the correct widgets appear based on state (loading, error, success).
  • Test User Interactions – Confirm that taps, scrolls, and inputs dispatch the right events.
  • Catch Integration Bugs – Detect mismatches between BLoC outputs and UI expectations.
  • Improve Confidence – Refactor UI safely knowing tests catch regressions.
  • Document UI Flows – Tests serve as executable specifications.

Setting Up Testing Environment

Add the necessary 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 Simple Counter Widget with Cubit

Start with a simple counter app using a Cubit. In widget tests, you provide the Cubit using BlocProvider and interact with the widget.

DARTRead-only
1
// counter_cubit.dart
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  void increment() => emit(state + 1);
}

// counter_page.dart
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: BlocBuilder<CounterCubit, int>(
          builder: (context, count) {
            return Text('Count: $count');
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterCubit>().increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}
DARTRead-only
1
// counter_page_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:mocktail/mocktail.dart';

class MockCounterCubit extends MockCubit<int> implements CounterCubit {}

void main() {
  group('CounterPage', () {
    late MockCounterCubit mockCubit;

    setUp(() {
      mockCubit = MockCounterCubit();
    });

    testWidgets('displays initial count from cubit', (tester) async {
      when(() => mockCubit.state).thenReturn(5);

      await tester.pumpWidget(
        MaterialApp(
          home: BlocProvider.value(
            value: mockCubit,
            child: CounterPage(),
          ),
        ),
      );

      expect(find.text('Count: 5'), findsOneWidget);
    });

    testWidgets('calls increment when button is pressed', (tester) async {
      when(() => mockCubit.state).thenReturn(0);

      await tester.pumpWidget(
        MaterialApp(
          home: BlocProvider.value(
            value: mockCubit,
            child: CounterPage(),
          ),
        ),
      );

      await tester.tap(find.byType(FloatingActionButton));
      verify(() => mockCubit.increment()).called(1);
    });
  });
}

Testing Async BLoC with Multiple States

For BLoCs that emit multiple states (loading → success/error), you need to simulate the state changes in tests. Use blocTest to define the expected states, and in widget tests, you can manually emit states to the mock.

DARTRead-only
1
// posts_bloc.dart
class PostsBloc extends Bloc<PostsEvent, PostsState> {
  final PostsRepository repository;
  PostsBloc(this.repository) : super(PostsInitial());
  // ...
}

// posts_page.dart
class PostsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<PostsBloc, PostsState>(
      builder: (context, state) {
        if (state is PostsLoading) {
          return Center(child: CircularProgressIndicator());
        } else if (state is PostsSuccess) {
          return ListView.builder(
            itemCount: state.posts.length,
            itemBuilder: (_, i) => ListTile(title: Text(state.posts[i].title)),
          );
        } else if (state is PostsError) {
          return Center(child: Text('Error: ${state.message}'));
        }
        return Container();
      },
    );
  }
}
DARTRead-only
1
// posts_page_test.dart
class MockPostsBloc extends MockBloc<PostsEvent, PostsState> implements PostsBloc {}

void main() {
  late MockPostsBloc mockBloc;

  setUp(() {
    mockBloc = MockPostsBloc();
  });

  testWidgets('shows loading indicator initially', (tester) async {
    when(() => mockBloc.state).thenReturn(PostsLoading());

    await tester.pumpWidget(
      MaterialApp(
        home: BlocProvider.value(
          value: mockBloc,
          child: PostsPage(),
        ),
      ),
    );

    expect(find.byType(CircularProgressIndicator), findsOneWidget);
  });

  testWidgets('shows posts list on success', (tester) async {
    final posts = [Post(id: 1, title: 'Post 1'), Post(id: 2, title: 'Post 2')];
    when(() => mockBloc.state).thenReturn(PostsSuccess(posts));

    await tester.pumpWidget(
      MaterialApp(
        home: BlocProvider.value(
          value: mockBloc,
          child: PostsPage(),
        ),
      ),
    );

    expect(find.text('Post 1'), findsOneWidget);
    expect(find.text('Post 2'), findsOneWidget);
  });

  testWidgets('shows error message on failure', (tester) async {
    when(() => mockBloc.state).thenReturn(PostsError('Something went wrong'));

    await tester.pumpWidget(
      MaterialApp(
        home: BlocProvider.value(
          value: mockBloc,
          child: PostsPage(),
        ),
      ),
    );

    expect(find.text('Error: Something went wrong'), findsOneWidget);
  });
}

Testing Widgets That Dispatch Events

To test user interactions that dispatch events, you can use blocTest to verify the events are added. In widget tests, you often use tester.tap and then verify that the event was added to the bloc.

DARTRead-only
1
testWidgets('tapping add button dispatches AddTodo event', (tester) async {
  final mockBloc = MockTodoBloc();
  when(() => mockBloc.state).thenReturn(TodoInitial());

  await tester.pumpWidget(
    MaterialApp(
      home: BlocProvider.value(
        value: mockBloc,
        child: TodoInputField(), // widget with TextField and add button
      ),
    ),
  );

  await tester.enterText(find.byType(TextField), 'Buy milk');
  await tester.tap(find.byType(ElevatedButton));
  await tester.pump();

  verify(() => mockBloc.add(AddTodo('Buy milk'))).called(1);
});

Testing BLoC Dependencies with Real Implementations

Sometimes you want to test the full integration with real repositories, but using a real API is slow. You can use a fake repository that returns predefined data, allowing you to test the BLoC and UI together.

DARTRead-only
1
class FakePostRepository implements PostRepository {
  @override
  Future<List<Post>> fetchPosts() async {
    return [Post(id: 1, title: 'Fake Post')];
  }
}

testWidgets('integration test with fake repository', (tester) async {
  final bloc = PostsBloc(FakePostRepository());

  await tester.pumpWidget(
    MaterialApp(
      home: BlocProvider.value(
        value: bloc,
        child: PostsPage(),
      ),
    ),
  );

  expect(find.byType(CircularProgressIndicator), findsOneWidget);
  await tester.pumpAndSettle();
  expect(find.text('Fake Post'), findsOneWidget);
});

Testing Navigation with BLoC

To test that a BLoC triggers navigation, you can use a MockNavigatorObserver or verify that the expected route was pushed. One approach is to wrap your app in a MaterialApp with a NavigatorObserver to capture pushes.

DARTRead-only
1
class MockNavigatorObserver extends NavigatorObserver {
  List<Route> pushedRoutes = [];

  @override
  void didPush(Route route, Route? previousRoute) {
    pushedRoutes.add(route);
    super.didPush(route, previousRoute);
  }
}

testWidgets('login success navigates to home', (tester) async {
  final mockObserver = MockNavigatorObserver();
  final mockBloc = MockAuthBloc();
  when(() => mockBloc.state).thenReturn(Authenticated());

  await tester.pumpWidget(
    MaterialApp(
      home: BlocProvider.value(
        value: mockBloc,
        child: LoginPage(),
      ),
      navigatorObservers: [mockObserver],
    ),
  );

  await tester.tap(find.byKey(Key('login_button')));
  await tester.pumpAndSettle();

  expect(mockObserver.pushedRoutes.length, 1);
  expect(find.byType(HomePage), findsOneWidget);
});

Best Practices

  • Use mocktail for mocking BLoCs – It works well with generics and sealed classes.
  • Provide BLoC via BlocProvider.value – This avoids creating a new instance in the test.
  • Test all UI states – Cover loading, empty, success, error, and edge cases.
  • Use pumpAndSettle – After dispatching events that cause multiple frame rebuilds, wait for animations to finish.
  • Test user interactions – Ensure events are dispatched correctly, not just UI appearance.
  • Avoid over-mocking – Sometimes a real, but controlled, BLoC with a fake repository is more realistic.
  • Keep tests isolated – Each test should have its own BLoC instance to avoid state leakage.
  • Use tester.pumpWidget with a clean MaterialApp – Provide necessary providers and themes.

Common Mistakes

  • ❌ Not calling pump after state changes – The widget tree needs to rebuild; use pump() or pumpAndSettle().
  • ❌ Using real network calls – Tests become slow and flaky; mock repositories or use fake data.
  • ❌ Forgetting to wrap with MaterialApp – Widgets that depend on Navigator or MediaQuery will fail.
  • ❌ Not providing a BlocProvider – Widgets using BlocBuilder will throw ProviderNotFoundException.
  • ❌ Mutating state across tests – Use a new BLoC instance per test or reset mocks properly.
  • ❌ Testing implementation details – Focus on observable behavior, not internal method calls unless necessary.

Conclusion

Widget testing with BLoC ensures that your UI correctly responds to state changes and user interactions. By combining mocktail for mocking, bloc_test for state verification, and Flutter’s widget testing framework, you can build a comprehensive test suite that covers both business logic and presentation layers. This leads to more robust, maintainable Flutter applications.

Try it yourself

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

// This is a simple app with a CounterCubit. To run widget tests,
// you would normally create a separate test file. This example
// includes the widget code for demonstration.

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

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

// -------------------- WIDGET --------------------
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter Widget')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BlocBuilder<CounterCubit, int>(
              builder: (context, count) {
                return Text(
                  'Count: $count',
                  style: TextStyle(fontSize: 32),
                );
              },
            ),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => context.read<CounterCubit>().increment(),
                  child: Text('+'),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () => context.read<CounterCubit>().decrement(),
                  child: Text('-'),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () => context.read<CounterCubit>().reset(),
                  child: Text('Reset'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

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

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

/*
  -------------------- WIDGET TEST EXAMPLE --------------------
  // test/counter_page_test.dart
  import 'package:flutter/material.dart';
  import 'package:flutter_test/flutter_test.dart';
  import 'package:bloc_test/bloc_test.dart';
  import 'package:mocktail/mocktail.dart';

  class MockCounterCubit extends MockCubit<int> implements CounterCubit {}

  void main() {
    group('CounterPage', () {
      late MockCounterCubit mockCubit;

      setUp(() {
        mockCubit = MockCounterCubit();
      });

      testWidgets('displays initial count', (tester) async {
        when(() => mockCubit.state).thenReturn(5);

        await tester.pumpWidget(
          MaterialApp(
            home: BlocProvider.value(
              value: mockCubit,
              child: CounterPage(),
            ),
          ),
        );

        expect(find.text('Count: 5'), findsOneWidget);
      });

      testWidgets('calls increment when + button is tapped', (tester) async {
        when(() => mockCubit.state).thenReturn(0);

        await tester.pumpWidget(
          MaterialApp(
            home: BlocProvider.value(
              value: mockCubit,
              child: CounterPage(),
            ),
          ),
        );

        await tester.tap(find.text('+'));
        verify(() => mockCubit.increment()).called(1);
      });

      testWidgets('calls reset when Reset button is tapped', (tester) async {
        when(() => mockCubit.state).thenReturn(10);

        await tester.pumpWidget(
          MaterialApp(
            home: BlocProvider.value(
              value: mockCubit,
              child: CounterPage(),
            ),
          ),
        );

        await tester.tap(find.text('Reset'));
        verify(() => mockCubit.reset()).called(1);
      });
    });
  }
*/

Test Your Knowledge

Q1
of 4

What is the correct way to provide a mock BLoC to a widget in a test?

A
BlocProvider(create: (_) => mockBloc)
B
BlocProvider.value(value: mockBloc)
C
BlocProvider.of<MyBloc>(context)
D
BlocBuilder<MyBloc, MyState>(builder: ...)
Q2
of 4

Which method is used to trigger a widget rebuild after emitting a new state in a test?

A
tester.flush()
B
tester.pump()
C
tester.rebuild()
D
tester.wait()
Q3
of 4

What package is recommended for mocking BLoCs in widget tests?

A
mockito
B
mocktail
C
mock_bloc
D
flutter_mock
Q4
of 4

What should you use to verify that a specific event was added to a BLoC after a user interaction?

A
expect() on bloc.state
B
verify() from mocktail
C
await tester.pump()
D
blocTest

Frequently Asked Questions

What's the difference between unit testing and widget testing BLoCs?

Unit tests test the BLoC in isolation (events, state transitions). Widget tests test the integration between the BLoC and the UI, verifying that the correct widgets are rendered based on state and that user interactions dispatch the right events.

How do I test a BLoC that uses `HydratedBloc` in widget tests?

You can mock the storage or use a temporary directory. For widget tests, it's easier to use a mock BLoC that doesn't rely on persistence, or provide a real but controlled storage (e.g., using HydratedBloc.storage = null).

Can I use `blocTest` inside a widget test?

Yes, but it's more common to use blocTest for unit testing BLoCs. For widget tests, you typically use the standard widget testing API with mocked BLoCs. However, you can combine both if needed.

How do I test asynchronous UI updates like loading indicators?

Use tester.pump() after emitting states to let the widget tree rebuild. For animations, use tester.pumpAndSettle() to wait for all animations to complete.

What if my widget uses multiple BLoCs?

Provide all required BLoCs using MultiBlocProvider in the test. Mock each BLoC separately and set up their states as needed.

Previous

bloc mocktail

Next

bloc freezed

Related Content

Need help?

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