Testing MobX State Tree
An experience-based story of how to test MobX State Tree
MobX State Tree is a state management solution for the front end world. If you want to learn more about it, have a look at this talk.
At DAZN we have been using MobX State Tree for more than a year now and this article is about what we learnt during the journey of writing all sort of tests. We’re going to analyse testing in a classic approach: starting from unit tests, then moving to integration tests and concluding with end to end tests.
Picking the right tool for the job is very important, but in this article I’d like to focus on different techniques to write tests with MobX State Tree. The libraries we’ll use in the examples – Jest, Enzyme and Testcafe – should not be the central part of the discussion and can be replaced at any time by your favourite one.
Part One – Unit tests
In MobX State Tree the unit is the store. Unit test means that a store works without any external dependency. If there is any kind of dependency on another module or file, it cant’t be consider a unit test anymore.
Let’s see how to test a Store with the following example
One thing to notice is that we’re creating a new instance of the store in each test. That means that tests are isolated and independent from others, which is always good.
Creating new stores at every test gives us a couple of benefits. The first one is that all the store lifecycle methods (eg.
afterCreate,afterAttachand so on ) will be called in the way they’re supposed to be called. The second is the possibility of passing an environment while creating the store, leveraging the dependency injection. This is very convenient if you want – for example – to mock backend responses, or simply override the normal environment that we’re passing.
Testing actions or views separately
In some cases our stores are quite complex (if they are too complex, you may think about splitting them) and we don’t want/need to create a new instance each time. In this case we can test separately actions or views, passing a mock version of
self to use in the tests.
Let’s have a look at how it works
fakeSelf used in the example stops to be our friend, and became our enemy. When we call
getEnv(self) we’ll get an error since
self is not a valid MobX State Tree but it’s our
fakeSelf object that we’re passing. One solution to that is intercept
getEnv and mock its response.
Unit tests, wrap up
Testing the store as single element could help you being more confident in a future refactor.
Testing just views and actions can be easy and fun, but please consider it only after you wrote some unit tests on the whole store. Writing views and actions tests only doesn’t tell you the whole story.
Testing the view
In the definition of unit tests, the concept of unit doesn’t apply only to business logic, but also to the view. in a MobX State Tree project, the views are React components.
In such a project, the recommended way to make store available for our views is to use the
Provider component from the
mobx-react npm package. This special components allows the injection of any props passed to it, into any component. In order to test the view, we leverage the fact that the injections can be easily overridden just by passing props to the component itself.
Lets have a look at the example
Testing views can be achieved in many ways such as using snapshot, screenshot and more. Talking about all the possibilities would diverge too much from the goal of this article: learn how to write test using MobX State Tree.
The thing to remember from the example is that we can override injection in our views, and once we have that, we can choose our favourite testing strategy and tools for the view.
Part two – Integration Tests
First of all, let’s start with a definition
When you write a test that involves multiple modules – but not the whole application – it’s an integration tests.
Writing integration tests for MobX State Tree means testing together two or more stores or stores and view together.
Integration means features
A classic approach for this kind of test is going vertical on a feature – from business logic to the views – and test how all the parts involved in delivering that feature work together.
Borrowing concepts from the different kind of unit tests described above – such as overriding injection & initialising stores – in the following example we can see how to test view and stores at the same time.
We create the store – which has children stores – and we pass it directly to our view. At this point we test that the view correctly display data, available in the store.
In a real-life scenario, integration tests are probably the hardest one to write. Wiring up together a subset of all the stores and a subset of all the views doesn’t come cheap, but it’s a good exercise to see how many connection we have between different features of your application.
Use the stores and the views you need, and mock everything else. If it gets too complicated, probably it’s time for an end to end test.
Part three – End to End tests
New kind of tests, new definition:
When you write a test that involves all the modules of your application, it’s an end to end test.
How can we possibly write a test that involves all the modules of our application? It’s actually very easy, you just have to load your application in a browser.
What? I’m not testing everything if I load my application in a browser!
In the moment we load the application, every element in the page is rendered so if you have an error anywhere in your code that would affect the test.
Which tool should I use ?
There are multiple articles about how to do end to end testing with different tool such as Selenium, Cypress or Testcafè, but at the end of the day they all do the same thing: you load your application at a specific url and you check that you have one or more elements in the page.
End to end tests should be framework and library agnostic. You can rewrite your application with Vue, React, Angular or plain Html, and your end to end tests should still all pass.
But if they are framework agnostic, why are we talking about MobX State Tree? The answer is: «snapshots» .
State hydration in end to end testing
Our applications have complex states and the ability to test your application end to end in one specific state it’s sometimes mandatory.
In order to achieve that, we need a state management system that allows snapshot & rehydration of the application state. A snapshot is like a photography of your application state in a particular moment in time and the rehydration is when you use that snapshot to load your application in the same situation when the snapshot was taken.
In MobX State Tree snapshots are first citizens and use them to rehydrate the state it’s very easy. We’re going to see two example, first the implementation of the snapshot & rehydration, and then we’re going to see how we can test it.
In the implementation above we’re using
onSnapshot to store our application state in the
localStorage every single time there’s a mutation of the state.
While loading the application, we check if there’s a particular state in the localStorage, and if it’s available we use that as first parameter on the
.create method, which is the initial state. MobX State Tree state will be created with an initial state taken from the
This allow us to write the following end to end test.
The third end to end test is where we do the state hydration. First we load the URL of our page and set the application state in a
localStorage item, then we reloads the application. At this point the first thing that our application does is checking if there’s a state in the
localStorage. Since a state is available, it will load the application in the state we provided. At this point, we only have the easy job deciding what to test.
- If writing tests is too complex it’s either a library problem or our code is hard to test, which usually means it’s hard to understand. Writing tests it’s a perfect time to check how easy-to-read our code is.
- With MobX State Tree we can and should write Unit, Integration and End to End tests!
- Create the stores in each Unit Test and if it gets too complicate, test action/views separately. Don’t forget the views!
- Testing views and stores with Integration Tests could be hard but it’s a good healthy check for the separation of concerns of our codebase.
- Snapshots & hydration of the application state in End to End Testing can help us testing complex flows.
- Github Repository: https://github.com/maxgallo/testing-mobx-state-tree
- Codesandbox: https://codesandbox.io/s/j7r6yzvqry
- MobX State Tree: https://github.com/mobxjs/mobx-state-tree
- Jest: https://github.com/facebook/jest
- Enzyme: https://github.com/airbnb/enzyme
- Testcafe: https://github.com/DevExpress/testcafe