Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: testing initial events #4229

Open
LukasMirbt opened this issue Aug 15, 2024 · 9 comments
Open

docs: testing initial events #4229

LukasMirbt opened this issue Aug 15, 2024 · 9 comments
Labels
documentation Documentation requested

Comments

@LukasMirbt
Copy link
Contributor

LukasMirbt commented Aug 15, 2024

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 LukasMirbt added the documentation Documentation requested label Aug 15, 2024
@tenhobi
Copy link
Collaborator

tenhobi commented Aug 16, 2024

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.

@LukasMirbt
Copy link
Contributor Author

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?

@tenhobi
Copy link
Collaborator

tenhobi commented Aug 16, 2024

@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(),
);

@LukasMirbt
Copy link
Contributor Author

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

@tenhobi
Copy link
Collaborator

tenhobi commented Aug 16, 2024

  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

@LukasMirbt
Copy link
Contributor Author

LukasMirbt commented Aug 16, 2024

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.

@felangel
Copy link
Owner

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! 👍

@LukasMirbt
Copy link
Contributor Author

LukasMirbt commented Aug 25, 2024

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
Copy link
Contributor Author

LukasMirbt commented Sep 12, 2024

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,
    );
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Documentation requested
Projects
None yet
Development

No branches or pull requests

3 participants