Similar to a previous article, in this one I would like to address some pain points I've encountered while working with the bloc package, specifically related to testing.
As you might have seen in the official bloc documentation, it promotes bloc_test package to facilitate writing unit tests.
After playing around with this package and the main testing facility it exposes, I couldn't write the kind of unit tests I was looking for.
So what's the deal 🤷🏽♂️
The problem is trying to assert that a certain state has being emitted independently of how current state has been built up or what its concrete value is.
blocTest
does seem to have an option to skip a number of previously emitted values and to seed the current state and start from there. But this is a bit annoying in my opinion since you might not now how many emissions have been sent for a particular state to be reached
and using seed might be error prone and difficult to build the correct seed.
I love using the matchers
package to do more flexible assertions, but to my surprise matchers didn't play around very nicely with blocTest
function (I might be missing something, leave a comment if you know a workaround)
With blocTest
you have to explicitly say what previous states have been (or skip them).
blocTest(
'emits [-1] when CounterEvent.decrement is added',
build: () => counterBloc,
act: (bloc) => bloc.add(CounterEvent.decrement),
expect: () => [-1], // the history of all emitted states up until now :(
);
I tried using matchers in the expect
parameter but did not work.
Solution 🏆
Much like the solution proposed in the previous article about mobx testing but this time wrapped in custom testing method.
This a what you get:
cubitTest('Emits loaded state when registration service succeeds',
build: () => sut,
stateField: (RegistrationState state) => state.status,
arrange: () {
when(
() => mockRegistrationService.register(
email: any(named: 'email'), password: any(named: 'password')),
).thenAnswer((_) async {
return;
});
sut.updateEmail('daniel@gmail.com');
sut.updatePassword('12345678');
},
act: (RegistrationCubit cubit) => cubit.register(),
assertions: (MockCallable<Status> updatesStatusWith) {
verify(() => updatesStatusWith(Status.loaded));
});
This example can work perfectly with either:
verify(() => updatesStatusWith(Status.loaded)),
// OR
verifyInOrder([
() => updatesStatusWith(Status.loading),
() => updatesStatusWith(Status.loaded),
]);
This is exactly what I want, I can do an assertion on particular property and don't need to reason about what the previous states have to be and how to get there.
Note in this example I'm using
mocktail
instead ofmockito
but either could be used. Same goes for cubit or bloc
Brief explanation
Very similar to blocTest
this also gives the same feel for arrange, act, assert structure but with some key differences, here is a short explanation of critical parameters:
stateField
is interesting because it allows selecting a the particular state property you're interested in.assertions
is a closure with a single parameter of type MockCallable where T will be whatever you selected withstateField
Note that stateField could be the identity function in which case you could do assertions over the entire state
Also you might think build
should return new instances of the bloc or cubit but I actually reuse the same instance for all tests so I can use the setup method and benefit from that as well.
And here is the implementation of the test wrapper:
github gist.
abstract class Callable<T> {
void call([T? arg]) {}
}
class MockCallable<T> extends Mock implements Callable<T> {}
@isTest
void cubitTest<P, S, B extends BlocBase<S>>(
String description, {
required B Function() build,
required FutureOr Function(B) act,
required P Function(S) stateField,
Function? arrange,
Function(P)? inspect,
required void Function(MockCallable<P> updatesWith) assertions,
}) async {
test(description, () async {
final expectation = MockCallable<P>();
arrange?.call();
final bloc = build();
bloc.stream.listen((S state) {
final focusedProperty = stateField(state);
inspect?.call(focusedProperty);
expectation(focusedProperty);
});
await act(bloc);
await bloc.close();
assertions(expectation);
});
}
Hope you liked this and found it helpful in some way!
Until next time...
Top comments (0)