bloc icon indicating copy to clipboard operation
bloc copied to clipboard

docs: testing initial events

Open LukasMirbt opened this issue 1 year ago • 9 comments

Description

Hi!

In #3944, it's mentioned that the recommended way to add initial events to a Bloc is as follows:

BlocProvider(
  create: (context) => MeetingsBloc()..add(LoadMettingsEvent()),
  child: MyWidget(),
);

The reasoning being that

this gives you granular control over when the event is added and makes testing easier and more predictable. In addition, it also makes the bloc more flexible and reusable.

However, this creates a new problem. How do you test that the correct initial events are added when the Bloc is created? As far as I know, none of the Bloc examples or documentation demonstrate a way to do this.

Is adding the initial events not considered worth testing?

Related issues: #3946, #3944, #3759, #2701, #2654, #1912, #1415

LukasMirbt avatar Aug 15 '24 10:08 LukasMirbt

For unit tests I would say it should not matter which first even you are adding to a bloc. After all, you should treat it as adding whichever event at whichever time.

You can test that in widget tests of i.e. your page, that the correct event is added. I would say mock the Bloc and check whether it is triggered.

tenhobi avatar Aug 16 '24 05:08 tenhobi

Out of curiosity, do you typically write (widget) tests for initial Bloc events?

You can test that in widget tests of i.e. your page, that the correct event is added. I would say mock the Bloc and check whether it is triggered.

Would you mind sharing a sample of what that would look like?

LukasMirbt avatar Aug 16 '24 10:08 LukasMirbt

@LukasMirbt good question. When I write widget tests, I would rather test what happends in what state. But you can for sure also test which event you have called when providing it, aka the first event. It is probably a usefull test since it tests that you didnt change the event etc. which might potentially break something. But I have never written such a test tbo :D

Something like this? writing it out of memorry in here, so it might not work 100 %, so take it rather as a concept:

late final MyBloc mockedBloc;

setUpAll(() {
  mockedBloc = MockMyBloc();
  when(mockedBloc.close).thenAnswer((_) => Future.value());

  // To make the page work, you have to provide some state that bloc builders would take.
  final state = SomeState();
  whenListen(
    mockedBloc,
    Stream.fromIterable([state]),
    initialState: state,
 );
});

patrolWidgetTest(
  'when state is AuthLoginStateFailure',
  ($) async {
    // arrange
    // pumpApp is extension method, imagine $.pumpWidget with MaterialApp etc.
    await $.pumpApp(const MyPage(myBloc: mockedBloc)); // pass the bloc, or mock get_it if you use that or whatever

    // act

    // assert
    verify(() => mockedBloc.add(MyFirstEvent())).called(1);
  },
);

imagine that you in that MyPage() you have something like this, myBloc from that parameter (or use get_it)

BlocProvider(
  create: (context) => myBloc..add(MyFirstEvent()),
  child: MyWidget(),
);

tenhobi avatar Aug 16 '24 10:08 tenhobi

When I write widget tests, I would rather test what happends in what state. But you can for sure also test which event you have called when providing it, aka the first event. It is probably a usefull test since it tests that you didnt change the event etc. which might potentially break something. But I have never written such a test tbo :D

Interesting! I also don't currently write any tests for this scenario even though I think it would be useful. For example, it's easy to forget to add the Started event and I often only catch this by manually testing. It would also be useful to be able to catch accidentally removing or adding the wrong initial event with unit/widget tests.

Something like this? writing it out of memorry in here, so it might not work 100 %, so take it rather as a concept:

Something like that might work but there are some subtle issues;

  • Where would myBloc be created?
  • Which BuildContext would be used for accessing repositories to inject into the Bloc?
  • Since myBloc is created outside of the BlocProvider, the lazy parameter wouldn't work
  • Using create (instead of the .value constructor) for an existing instance opens up for potential issues related to how the bloc is disposed

LukasMirbt avatar Aug 16 '24 11:08 LukasMirbt

  1. in the example its passed by parameter to the provider. You can make it nullable and do myBloc ?? MyBloc() in the provider create. Or rather use get_it and just inject a factory instance there -- then you also need to mock get it in test so it provides you the instance. We use this function to do it:
void mockInLocator<T extends Object>(T mock) {
  when(
    () => GetIt.I.get<T>(
      param1: any<dynamic>(named: 'param1'),
      param2: any<dynamic>(named: 'param2'),
    ),
  ).thenReturn(mock);
  when(
    () => GetIt.I.call<T>(
      param1: any<dynamic>(named: 'param1'),
      param2: any<dynamic>(named: 'param2'),
    ),
  ).thenReturn(mock);
}

// for example, in set up
mockInLocator<MyBloc>(mockedBloc);
  1. Since you are mocking the bloc, you would not provide it any repository to it. You would do class MockMyBloc extends MockBloc implements MyBloc {}

  2. I would use get_in in real app, that way it would work.

  3. Yes, this was just an example from my mind. :D I would use create with get_it to access factory instance of the bloc

tenhobi avatar Aug 16 '24 12:08 tenhobi

I would use get_in in real app, that way it would work. Yes, this was just an example from my mind. :D I would use create with get_it to access factory instance of the bloc

I understand, thanks for taking the time to create a sample 👍 I personally don't use get_it. It would be ideal to have a solution that uses BlocProvider, since it's built into the Bloc library.

LukasMirbt avatar Aug 16 '24 13:08 LukasMirbt

Hi @LukasMirbt 👋 Thanks for opening an issue!

Generally, I agree with @tenhobi's suggestions to test the initial event is added in your widget tests since this functionality of part of the widget tree (not the bloc itself). An example of such a test can be found in the flutter_todos example.

This is also something that would be caught by adding integration tests to your flutter app. Hope that helps! 👍

felangel avatar Aug 25 '24 22:08 felangel

Generally, I agree with @tenhobi's suggestions to test the initial event is added in your widget tests since this functionality of part of the widget tree (not the bloc itself).

I agree, that sounds very reasonable 👍

An example of such a test can be found in the flutter_todos example.

This widget test seems to verify repository behavior. I would prefer to avoid that since this would couple the widget test to domain layer details.

It would be great to be able to test the interface to the Bloc directly, similar to this.

Let me know what you think!

LukasMirbt avatar Aug 25 '24 23:08 LukasMirbt

Something similar to the TestableBlocProvider implementation below would enable writing widget tests like this:

class MockCounterBloc extends MockBloc<CounterEvent, CounterState>
    implements CounterBloc {}

void main() {
  group(CounterPage, () {
    late CounterBloc counterBloc;

    setUp(() {
      counterBloc = MockCounterBloc();
      when(() => counterBloc.state).thenReturn(CounterState());
    });

    Widget buildSubject() => MaterialApp(home: CounterPage());

    testWidgets('adds $CounterStarted when $CounterBloc is created',
        (tester) async {
      await tester.pumpWidget(buildSubject());
      final blocProvider = tester.widget<TestableBlocProvider<CounterBloc>>(
        find.byType(TestableBlocProvider<CounterBloc>),
      );
      blocProvider.onCreated(counterBloc);
      verify(() => counterBloc.add(CounterStarted())).called(1);
    });
  });
class TestableBlocProvider<T extends StateStreamableSource<Object?>>
    extends StatelessWidget {
  const TestableBlocProvider({
    required this.create,
    required this.onCreated,
    required this.child,
    super.key,
  });

  final T Function(BuildContext context) create;
  final void Function(T bloc) onCreated;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return BlocProvider<T>(
      create: (context) {
        final bloc = create(context);
        onCreated(bloc);
        return bloc;
      },
      child: child,
    );
  }
}

LukasMirbt avatar Sep 12 '24 13:09 LukasMirbt