docs: testing initial events
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
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.
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 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(),
);
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
myBlocbe created? - Which
BuildContextwould be used for accessing repositories to inject into the Bloc? - Since
myBlocis created outside of theBlocProvider, thelazyparameter wouldn't work - Using
create(instead of the.valueconstructor) for an existing instance opens up for potential issues related to how the bloc is disposed
- 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);
-
Since you are mocking the bloc, you would not provide it any repository to it. You would do
class MockMyBloc extends MockBloc implements MyBloc {} -
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 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.
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! 👍
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!
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,
);
}
}