How do I mock a bloc in Flutter, with states being emitted in response to events from a widget under test

Issue

I’m trying to test a widget that makes use of a bloc. I’d like to be able to emit states from my mocked bloc in response to events being fired by the widget under test. I’ve tried a number of approaches without success. I’m not sure if I’m making some simple error or if I’m approaching the problem all wrong.

Here is a simplified project which demonstrates my issue. (the complete code for this can be found at https://github.com/andrewdixon1000/flutter_bloc_mocking_issue.git)

very simple bloc

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';

class MyBloc extends Bloc<MyEvent, MyState> {
  MyBloc() : super(FirstState());

  @override
  Stream<MyState> mapEventToState(
    MyEvent event,
  ) async* {
    if (event is TriggerStateChange) {
      yield SecondState();
    }
  }
}

@immutable
abstract class MyEvent {}

class TriggerStateChange extends MyEvent {}


@immutable
abstract class MyState {}

class FirstState extends MyState {}

class SecondState extends MyState {}

My widget under test

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

import 'bloc/my_bloc.dart';
import 'injection_container.dart';

class FirstPage extends StatefulWidget {
  const FirstPage({Key? key}) : super(key: key);

  @override
  _FirsPageState createState() => _FirsPageState();
}

class _FirsPageState extends State<FirstPage> {

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => serviceLocator<MyBloc>(),
      child: Scaffold(
        appBar: AppBar(title: Text("Page 1")),
        body: Container(
          child: BlocConsumer<MyBloc, MyState>(
            listener: (context, state) {
              if (state is SecondState) {
                Navigator.pushNamed(context, "SECONDPAGE");
              }
            },
            builder: (context, state) {
              if (state is FirstState) {
                return Column(
                  children: [
                    Text("State is FirstState"),
                    ElevatedButton(
                        onPressed: () {
                          BlocProvider.of<MyBloc>(context).add(TriggerStateChange());
                        },
                        child: Text("Change state")),
                  ],
                );
              } else {
                return Text("some other state");
              }
            },
          ),
        ),
      ),
    );
  }
}

my widget test
This is where I’m struggling. What I’m doing is loading the widget and then tapping the button. This causes the widget to add an event to the bloc. What I want to be able to do is have my mock bloc emit a state in response to this, such that the widget’s BlocConsumer’s listener will see the state change the navigate. As you can see from the comment in the code I’ve tried a few things without luck. Current nothing I’ve tried results in the listener seeing a state change.

import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart' as mocktail;
import 'package:get_it/get_it.dart';
import 'package:test_bloc_issue/bloc/my_bloc.dart';
import 'package:test_bloc_issue/first_page.dart';

class MockMyBloc extends MockBloc<MyEvent, MyState> implements MyBloc {}
class FakeMyState extends Fake implements MyState {}
class FakeMyEvent extends Fake implements MyEvent {}

void main() {
  MockMyBloc mockMyBloc;
  mocktail.registerFallbackValue<MyState>(FakeMyState());
  mocktail.registerFallbackValue<MyEvent>(FakeMyEvent());
  mockMyBloc = MockMyBloc();

  var nextScreenPlaceHolder = Container();

  setUpAll(() async {
    final di = GetIt.instance;
    di.registerFactory<MyBloc>(() => mockMyBloc);
  });

  _loadScreen(WidgetTester tester) async {
    mocktail.when(() => mockMyBloc.state).thenReturn(FirstState());
    await tester.pumpWidget(
      MaterialApp(
        home: FirstPage(),
        routes: <String, WidgetBuilder> {
          'SECONDPAGE': (context) => nextScreenPlaceHolder
        }
      )
    );
  }

  testWidgets('test', (WidgetTester tester) async {
    await _loadScreen(tester);
    await tester.tap(find.byType(ElevatedButton));
    await tester.pump();
    
    // What do I need to do here to mock the state change that would
    // happen in the real bloc when a TriggerStateChange event is received,
    // such that the listener in my BlocConsumer will see it?
    // if tried:
    // whenListen(mockMyBloc, Stream<MyState>.fromIterable([SecondState()]));
    // and
    // mocktail.when(() => mockMyBloc.state).thenReturn(SecondState());
    await tester.pumpAndSettle();

    expect(find.byWidget(nextScreenPlaceHolder), findsOneWidget);
  });
}

Solution

I took a look and opened a pull request with my suggestions. I highly recommend thinking of your tests in terms of notifications and reactions. In this case, I recommend having one test to verify that when the button is tapped, the correct event is added to the bloc (the bloc is notified). Then I recommend having a separate test to ensure that when the state changes from FirstState to SecondState that the correct page is rendered (the UI reacts to state changes appropriately). In the future, I highly recommend taking a look at the example apps since most of them are fully tested.

Answered By – Felix Angelov

Answer Checked By – Mary Flores (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *