Hello,
Currently blocTest does not allow for post test verify or other expect statements. Instead this has to be done in the teardown (or in some cases can be done in the blocTest act) callbacks, which works but is not ideal.
I suggest to add a callback to blocTest and call it at the end of the test
@isTest
void blocTest<B extends Bloc<Event, State>, Event, State>(
String description, {
@required B Function() build,
@required Iterable expect,
Future<void> Function(B bloc) act,
Future<void> Function(B bloc) verify,
Duration wait,
}) {
test.test(description, () async {
final bloc = build();
final states = <State>[];
final subscription = bloc.listen(states.add);
await act?.call(bloc);
if (wait != null) await Future.delayed(wait);
await bloc.close();
test.expect(states, expect);
await subscription.cancel();
await verify?.call(bloc);
});
}
With this you can verify if a bloc with a mocked repository _actually performs the correct calls_.
To give you an idea, a login bloc test could verify if a token has been saved:
MockRepository repository;
setUp((){
repository = MockRepository();
});
blocTest(
'Login',
build: (){
when(repository.login(any)).thenAnswer((_) async => 'session token');
return LoginBloc(repository);
},
act: (bloc) => bloc.add(Login(user: 'user', password: 'password')),
expect: [Idle(), LoginSuccessful()],
verify: (_) async {
verify(repository.login(any)).called(1);
verify(repository.saveToken(any)).called(1);
}
);
Here I make sure that the token is saved. This could also be useful when certain functions are called more than once.
I would love to hear your thoughts on this small improvement!
I may be wrong but this kind of test is not responsability of a blocTest. A bloc test should check the inputs (Events) and outputs (States), more precisaly you should be testing what states are beeing yielded when the events are added.
Hi @rodrigobastosv
Thank you for your input. I can see why you think it might not be the responsibility of the blocTest to verify function calls, and perhaps you're right. I think however that when testing blocs, you shouldn't just test its inputs and outputs, but also how it transforms said input to its output.
It is probably enough to test if each input event gives the correct output state in most cases. But, in the little example I have provided, if you were to remove the verify callback, the test would still succeed if in the future the login token is not saved. This could lead to issues elsewhere, so I would like to capture these issues as quickly as possible.
Hey @ThomasvanWanrooij i completely got your point. Let's see what @felangel have to say about that
Hi @ThomasvanWanrooij 👋
Thanks for opening an issue!
I definitely think you make a valid point but do you think the verify callback would need to have access to the bloc? I would expect all bloc related expectations to be made in expect and verify to be reserved for "other expectations". Let me know what you think and thanks for bringing this up!
After a while, I think this is actually a good idea. Checking for repository calls or whatever is a valid feature and should (may) be part of your Bloc test. Otherwise, you would need to make a regular test and where is the point of blocTest then, if you would use it only sometimes. :-)
Agree with @felangel that you should not be given the bloc instance. All the things like repositories etc. are provided using the constructor (or should be), so you already have everything you have.
@felangel Providing the bloc instance is probably not necessary in most cases. As @tenhobi already mentioned, most repositories etc that is used for verification will be passed in its constructor. I have used it in some places to access @visibleForTesting and public variables though.
This is something I do in a chain of automated events, where I need to set an internal flag (i.e. not exposed via states or events) somewhere early on in the chain, but only use it to add additional data in the last output state. I have tests for the specific steps and want to make sure the correct step sets the flag in a correct manner.
Now this flag can also be checked in the expect with the output states if you were to create a test that encapsulates the entire chain, but this is less specific as to when that flag has been changed as you would only be able to expect whether the additional data is provided or not.
I can't really think of other cases this would be useful for though, so not using blocTest for this particular test case is not a huge problem.
OK, @visibleForTesting is a good point. I think we might provide that instance, but write docs, etc., with the fact that you should consider if you want to ever use it. 🤷♂️
@felangel @tenhobi So, I have had some more time to play around with this more and you actually don't need to provide the bloc instance.. In the case of @visibleForTesting, this can easily be done by defining the bloc outside blocTest, and assigning it in the `build function.
MockRepository repository;
LoginBloc bloc;
setUp((){
repository = MockRepository();
});
tearDown((){
bloc = null;
});
blocTest(
'Login',
build: (){
when(repository.login(any)).thenAnswer((_) async => 'session token');
bloc = LoginBloc(repository);
return bloc;
},
act: (bloc) => bloc.add(Login(user: 'user', password: 'password')),
expect: [Idle(), LoginSuccessful()],
verify: () async {
verify(repository.login(any)).called(1);
verify(repository.saveToken(any)).called(1);
expect(bloc.token, isNotNull);
}
);
Cool!
Addressed in #804 and published in bloc_test v3.1.0 🎉
Most helpful comment
Hi @ThomasvanWanrooij 👋
Thanks for opening an issue!
I definitely think you make a valid point but do you think the verify callback would need to have access to the bloc? I would expect all bloc related expectations to be made in
expectandverifyto be reserved for "other expectations". Let me know what you think and thanks for bringing this up!