Better Bloc and Cubit Unit Testing

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('[email protected]');
      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 of mockito 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 with stateField

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> {}


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...

18