Warning ⚠
None of what you see below may be applicable to a reasonable Flutter developer. It may well be the case that I'm the only one who needs the following changes. However, I plan to use the changed
blocTestin some of my tutorials.
First up, I find that having to specify initial states in every blocTest is redundant. It would be better if they were skipped. Testing the initial state is always possible with the initialState property in a regular test, no need to create a blocTest.
Secondly, it's useful to have the Bloc instance readily available inside the verify function. Consider:
blocTest(
"should register using IAuthFacade if email and password are valid",
build: () async {
arrangeAuthFacadeSuccess();
await prefillWithValidEmailAndPassword();
return signInFormBloc;
},
act: (bloc) async =>
bloc.add(const SignInFormEvent.registerWithEmailAndPasswordPressed()),
verify: (SignInFormBloc bloc) async {
verify(mockAuthFacade.registerWithEmailAndPassword(
emailAddress: argThat(
equals(bloc.state.validatedEmailAddress),
named: 'emailAddress',
),
password: argThat(
equals(bloc.state.validatedPassword),
named: 'password',
),
));
},
);
The above snippet of code brings me to my third point - making the build function asynchronous. With Blocs that have only one state class, such as the one from the Firebase login tutorial, it may be useful to initialize (or "prefill") the Bloc into a certain state. See the prefilling method:
Future<void> prefillWithValidEmailAndPassword() async {
signInFormBloc.add(const SignInFormEvent.emailChanged('[email protected]'));
signInFormBloc.add(const SignInFormEvent.passwordChanged('pazzwrd'));
await signInFormBloc.take(3).last;
}
As the events which "prefill" the Bloc will trigger state changes in which I'm not interested as they're tested separately, I need to ignore them using the take(number).last contraption. This is the reason why the build function has to be async.
Here's my proposed modified blocTest. I'm open to splitting it up into a separate function as not to force everyone into using it.
@isTest
void blocTest<B extends Bloc<Event, State>, Event, State>(
String description, {
@required Future<B> Function() build,
// not required - sometimes we want to just verify without expecting
Iterable expect,
Future<void> Function(B bloc) act,
Duration wait,
Future<void> Function(B bloc) verify,
}) {
test.test(description, () async {
final bloc = await build();
final states = <State>[];
// Skipping the initial state
final subscription = bloc.skip(1).listen(states.add);
await act?.call(bloc);
if (wait != null) await Future.delayed(wait);
await bloc.close();
if (expect != null) test.expect(states, expect);
await subscription.cancel();
await verify?.call(bloc);
});
Hey @ResoDev 👋
Thanks for opening an issue!
You make some great points and I agree that blocTest should be able to work seamlessly in the situations you’ve described 👍
I’m out of town until tomorrow so let me think about your proposal a bit and brainstorm some potential alternatives and I’ll check back sometime tomorrow.
I'm liking the proposal @ResoDev, as I have similar cases where I want to just test specific functionality and "priming" the bloc to a specific point to test a specific event as a smell to me, instead of just being able to specify that I want the bloc at a specific state and then test from said state with the passed in event.
To summarize, I'd also like to be able to set the state of the bloc before my specific unit test runs.
Opened #913 to address the requested changes 👍
Would love it if y'all could take a look and provide any feedback 🙏
Most helpful comment
I'm liking the proposal @ResoDev, as I have similar cases where I want to just test specific functionality and "priming" the bloc to a specific point to test a specific event as a smell to me, instead of just being able to specify that I want the bloc at a specific state and then test from said state with the passed in event.
To summarize, I'd also like to be able to set the state of the bloc before my specific unit test runs.