Testing strategies in Redux
Published by Carlos Blé on 05/04/2017
In the previous post I explained how to test-drive a Redux app from the outside-in with an example of a coarse-grained test integrating most of the frontend layers. That was just one case but there are more cases, more kind of tests. These are the common things I like to check in my tests:
- GUI events or componentDidMount ends up with…
- consistent changes in the store’s state.
- consistent changes in the component’s state.
- an async call to server, which response is properly handled.
- no action calls at all, just display validation errors instead.
- router redirections.
- Store’s state is correctly rendered in the GUI
Most of the items in the list are testable in integration using Enzyme’s mount function. Others however are better tested via Enzyme’s shallow, in fact I’ve experienced situations where testing with mount is not possible because the component’s state is not accessible for instance or because the behavior is unpredictable.
Let’s see an example. In our app, the component has a notifier object injected intended to show user messages such as the beginning of a potentially slow operation or the end of it. As some actions are is asynchronous, the test has to wait for all the things we want to happen. Waiting is possible thanks to the done argument provided by the test framework - jest, mocha, jasmine…
When the expectation is not met, the test fails with a timeout because done is never called. However the failed expectation is also shown in the console. It’s a bit tricky because one have to scroll a bit to reach the actual failure reason in the output.
We could further refactor the test above to make it less verbose and easier to change:
The next scenario is similar just a bit more complex. When the component loads, it must fetch data from server to populate the store with it. This is done inside componentDidMount.
The action is async, data is fetched from server and then dispatched via Redux. Thanks to redux-thunk middleware, the promise is resolved once the store has been updated. This is why we can expect the GUI to be refreshed. In the next blog post I’ll write about the various kinds of actions. The test can be refactored like before. I leave that for you.
We could have written a finer test in terms of scope. We could have been checked that server data has populated the store correctly:
We’ve got access to the store instance so we can subscribe to it and make assertions with the final state. Which test is best? it depends on several aspects. I prefer to test-drive with coarse-grained tests but if I feel the need to debug despite of writing small tests, I go for a finer test like this. It’s a trade-off, confidence, brittleness and ease to understand and fix.
There are occasions where checking the store is the only choice. One example is the logout page:
On line 8 I expect the store to be properly populated, before exercising the code under test. As the logout action could be asynchronous, I wait for the store to change via subscribe and then make the assertion.
Up until now all the tests are mounting the component and running asynchronous tests. This technique is good enough when components are simple. Once they get more complex, for instance when they manage itw own state calling this.setState, small isolated tests are required. In these tests I usually stub or spy on actions thus testing the component in isolation. The next example tests a form that dynamically renders input text boxes for every email in a list of emails:
If one of the emails is going to be removed from the list using the form, I want to make sure the action receives the correct list:
Synchronous tests are easier to reason about.
I’ve experienced unpredictable behavior mixing both, synchronous and asynchronous tests in the same suite when the system under test contains components that manage their own state. It’s important to remember that React’s setState is asynchronous although the name of the function doesn’t look like it is. I haven’t got the time to analyze the problem and see whether it’s in React Test Utilities, in Enzyme,… but I just try to keep those tests in separate test suites.