Daniel Cardona Rojas
Posted on April 30, 2021
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();
}
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());
});
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> {}
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()),
]);
});
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())))
);
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);
});
}
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!
Posted on April 30, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.