Low effort, high value. Integration tests in Redux apps.

13,623 reads

tldr; You can test your Redux app by rendering it in node, simulating user interactions and verifying changes in state and markup. These tests are relatively easy to write, fast to run and give a lot of confidence.

Writing efficient software tests is a tricky balancing act. By efficiency, I don’t mean execution speed or resource consumption, but rather nailing the trade-off between the effort put into writing tests and the value they provide.

This is not a new or unknown problem. A lot of smart people pondered on it in the past, and established guidelines that can help developers tackle it. I’m a big believer in the testing pyramid that dictates the relative number of different types of tests in a healthy test suite, with unit tests being the strong base, covering every piece of code individually.

Unit tests and Redux

The structure encouraged by Redux makes writing unit tests a breeze. You can require different blocks of the application (think reducers, action creators, containers, etc.) in isolation, and test them like any other pure function — test data in, asserting on the data out, no mocking required. The testing guide in Redux documentation lists unit testing approach for each of these blocks.

Following this guide you can get to a complete unit test coverage by tediously copy-pasting tests from reducer to reducer, from action creator to action creator... But once all that work is done, the testing pyramid strikes back. Since it’s only unit tests, the test suite still doesn’t answer the fundamental question — does the app actually work?

Climbing the pyramid

There are several ways to interpret the upper layers of the testing pyramid in the context of a webapp. The top, end-to-end (e2e) layer can be implemented using Selenium, for example with webdriver.io. These tests are technology agnostic, so they will still be valid even if you port your app to use a different framework. However, they take long to implement and run, are hard to debug, and can often be flaky. Usually a project can and should only afford a relatively small number of them.

What about the layer between unit and e2e tests? In general, these are called integration tests, as they verify how different modules in the application work together. The spectrum of integration tests is wide. For example, one could argue that if a test for reducers uses action creators to dispatch actions, it’s already more than a unit test. On the other end of that spectrum, e2e test can be seen as the most extreme case of an integration test.

It seems that one could try to find the sweet-spot for integration tests in Redux. Ideally, they should be fast enough to run as a part of the development flow, they should use the same testing infrastructure as the unit tests and they should give a decent level of confidence that the entire part of the webapp managed by Redux works as expected.

Finding the boundaries

Finding out where to place the boundaries of the tests is a good starting point. The structure of most webapps can be represented like so:

Typical high-level structure of a webapp

Some parts of the system need to be mocked, in order to achieve the desired characteristics of the tests. From the top, the most limiting factor is the browser. Starting an instance of (even a headless) browser to run the tests will take much longer than running some code in node. From the bottom, we don’t want to wait for real requests to complete. The network layer is also a clearly defined interface that is reasonably easy to mock.

Mocking the boundaries

Assuming the app uses React and Redux, it’s quite easy to write it in a way that allows it to smoothly run in node during testing (or even in production if you’re rendering server-side). This means it’s possible to use the great Jest testing framework to run the tests as well as enzyme to render parts, or the entirety of your application and interact with it without the need of an actual browser environment.

Enzyme provides a mount function which can be used to render and interact with any React component, for instance, a complete Redux app. To reduce the boilerplate for each test, it’s useful to write a simple utility function that renders the app with given state and returns the enzyme’s wrapper object, as well as the Redux store object (which will come handy in assertions).

Utility for rendering Redux application with arbitrary state, and an example of usage

Running the tests in node also enables some clean mocking solutions for the network layer, e.g. the nock library. Nock makes it easy to declare response data and codes as well as errors for network requests before running specific tests. Mocking a successful GET request can look like this:

Example of mocking a network request with nock

With this setup, it should be possible to run the integration tests with the convenience and speed not that much worse than the unit tests. All that’s left is implementation…

Mocked boundaries for Redux integration tests

What to test?

The kind of integration tests that will give the most confidence in the correct functioning of the app are those that take the perspective of the user. The goal is to verify that once the user interacts with the application by clicking buttons, filling form elements etc., the app responds by modifying itself or performing side effects as expected.

Let’s consider a simple scenario of submitting a form. We render the app with the data already filled in, and simulate the user clicking the submit button. We also make sure that the request to the API endpoint that our app is wired to call will succeed.

Boilerplate for the integration test with Jest

When to test?

Before diving into implementation of the assertions, there is one more problem to address: when to run them. In a simple case, when all the changes in the app happen synchronously, you can run the assertions straight after simulating the user action. However, your app will most likely use Promises to handle async part of the code, e.g. the network requests. Even though the request is mocked to resolve synchronously, the success promise handler will run strictly after any code that sits right below the submitButton.simulate('click') line. We need to wait for our app to “be done” before we start asserting.

Jest offers several ways of working with async code, but they require either a direct handle to the promise chain (which we don’t have in this example) or require mocking timers (which doesn’t work with promise-based code). You could use setTimeout(() => {}, 0) which forces us to use Jest’s async callback feature, but this will make the test code much less elegant.

However, there is a nice solution to this problem in a form of a one-liner utility function that creates a promise resolved on the next tick of the event loop. We can use it with Jest’s built-in support for returning a promise from the test:

Elegant solution to running assertions for Promise based code without handle to the Promise chain

How to test?

What options are there, for verifying the app responded to the user interaction correctly?

Markup. You can inspect the markup of the page to check that the UI is correctly modified, for example using Jest’s snapshot feature. Note: for the following test to work you will need to setup a Jest snapshot serializer, for example using enzyme-to-json package.

Using Jest’s snapshot feature to verify markup changes

This kind of assertions is incredibly easy to write, but tests using them tend to be quite unfocused. The snapshot of the app’s markup will probably change often, making your seemingly unrelated tests fail. They also don’t document the expected behaviour, only verify it.

State. Check the modification to the state of the application. It’s easy in a Redux application with a centralised store, might be more tricky if the state is distributed. Also in this case snapshot could be used, but I prefer the explicitness of object literals.

Verifying changes in Redux store state

This types of assertion is less user-centric, as the store state sits “under the hood” of your application. However, testing like this will be less susceptible to flakiness caused by design-driven changes to markup.

Side effects. Depending on your application, there may be other side effects that you should check (e.g. network requests, changes to localStorage ). You could, for example, use the isDone method from nock to verify that the request mocks created have been consumed.

Dispatched actions. This approach takes advantage of one of the strongest features of Redux, the serialisable log of actions. We can use it to assert on the sequence of the actions dispatched to the store, e.g. with help of the useful redux-mock-store library. First, the renderAppWithState method needs to be modified to use a mocked version of Redux store, so that the store exposes a getActions method.

Verifying the sequence of actions dispatched to the store

This type of assertions is useful especially for more complex async flows. It also provides a clear overview of the expected behaviour of the app in the tested scenario, serving as documentation.

Finding the balance

Introduction of this type of integration tests should not mean skipping unit tests. Most parts, and especially the logic-heavy parts of the application (like reducers or selectors in a Redux app) still require to be thoroughly unit-tested. The pyramid still applies! However, integration tests are a valid addition to the testing toolbox that should help building a healthy test suite that causes as little pain as possible and allows for more confident deployments.

The subject of software testing is one of the most opinionated in the industry. When reviewing this article, one of my colleagues pointed by to an article titled “Integrated tests are a scam”. Some of the points the author makes are valid, but things are not so black-and-white in my opinion. What do you think?

If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!

Tags