DEV Community

Daniel Cardona Rojas
Daniel Cardona Rojas

Posted on • Updated on

Testing Mobx stores in Flutter

I've been using mobx in a couple of projects and have enjoyed its pragmatic and boilerplate free approach to state management.

Having said that when it came to testing I did not find a good way to write the kind of tests I wanted, although the documentation for mobx in Flutter is great https://mobx.netlify.app/ I found it lacking when trying to figure out how write robust unit tests. Luckily I found quite a nice approach that I'd like to share in this post.

The main requirement I had was to be able to test multiple state changes within a single action in a store.

Let me give a common example that motivates this requirement. Lets suppose we have a store that fetches items of a feed.

Most of the time we want to have some state property that reflects if the feed is in the loading, has failed to fetch items or has finally received the items from a service

The problem

In mobx stores an action can mutate an observable property multiple times

For example:

@observable
Status status = Iddle();

@observable
List<Item> items = [];

@action
Future<void> getItems() async {
  status = Loading();
  items = await _service.getItems();
  status = Loaded();
}

Enter fullscreen mode Exit fullscreen mode

Since state management in mobx unlike bloc is not stream based. Its not obvious how to check intermediate mutations of an observable property within the action. If you await the action you will only be able to inspect the final value of the inspected property.

test('store emits status values for loading, loaded when fetching items', () async {
  // arrangement code

  assert(store.status == Iddle());
  await store.getItems();

  // Check that status passed through Loading value

  assert(store.status == Loading()); // Will fail!!!
  assert(store.status == Loaded());

});
Enter fullscreen mode Exit fullscreen mode

Reactions 🧪 to the rescue 🎉

The solution comes down to using a mobx reaction to observe mutations of a property within the store.

I'll introduce an extra class that is required for this approach

import 'package:mockito/mockito.dart';

abstract class Callable<T> {
  void call([T arg]) {}
}

class MockCallable<T> extends Mock implements Callable<T> {}

Enter fullscreen mode Exit fullscreen mode

So going back to our example this is how the test could be written:

test('store emits status values for loading, loaded when fetching items', () async {
    final statusChanged = MockCallable<Status>();

    when(mockService.getItems())
        .thenAnswer((_) async => fakeItems);

    mobx.reaction<Status>(
        (_) => store.status, (newValue) => statusChanged(newValue));

    await store.getComments();

    verifyInOrder([
      statusChanged(Loading()),
      statusChanged(Loaded()),
    ]);

});
Enter fullscreen mode Exit fullscreen mode

This actually turns out to be easier to test then BLOC in my opinion, since bloc tests require you to specifically state the exact value for all emitted states. In mobx using this approach you can opt in to this by testing a single mutation or a sequence of mutations ocurring in an action using any of the matchers that are provided out of the box by mockito.

Here are some other examples assertions that could be made in other test that showcase the flexibility you get with different types of matchers.

// Or
verify(statusChanged(any)).called(2);

// Or
verify(
  statusChanged(argThat(allOf(Loaded(), Loading())))
);

// Or
verify(
  statusChanged(argThat(anyOf(Loaded(), Loading())))
);

Enter fullscreen mode Exit fullscreen mode

Edit: As a bonus here is a simple wrapper that can help get rid of some of the boilerplate setup:

@isTest
void mobxTest<S extends Store, P>(
  String description, {
  @required S Function() build,
  @required FutureOr Function(S) 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 store = build();
    reaction<P>((_) => stateField.call(store), (P newValue) {
      inspect?.call(newValue);
      expectation.call(newValue);
    });

    await act?.call(store);

    assertions(expectation);
  });
}
Enter fullscreen mode Exit fullscreen mode

I suggest looking into the matchers package to see all the cool matchers.

Personally like anyOf matchers which allows to check that a particular value has been emitted without having to check previous or future values.

I hope you liked this post, let me know your opinions, cheers!

Discussion (2)

Collapse
pablonax profile image
Pablo Discobar • Edited

Wow, cool article! If you are interested in this topic, then look here dev.to/pablonax/flutter-templates-...

Collapse
jamescardona11 profile image
James Cardona

Amazing bro.