Redux Testing: Hard-Earned Lessons Learned

To celebrate the launch of my new course Confidently Testing Redux Applications with Jest & TypeScript I wanted to share some of the lessons I've learned about testing over my years of using redux in production react applications.

Almost everything I learned through experience was already discovered by others and put down in the Redux Style Guide which I highly recommend reading and sharing with your teams.

In particular I want to share four things that have helped me get to a place where I feel like I'm testing the right things without a lot of hassle. Here's that list:

  1. Stop Testing your Disconnected Components
  2. Build a Small Utility Library
  3. Render Components with a Real Store
  4. Prefer Integration Style Tests

One of the difficult things about bringing Redux into your apps is that any redux-connected component needs to be wrapped at some level in a redux <Provider>. That Provider ensures all components rendered in that tree use the same redux store. When you're building an app, you usually just add <Provider> at the top level and don't have to worry about it. When testing redux apps though it becomes a major pain. Each test for a redux-connected component has to be individually wrapped in its own provider.

Many teams get around this by exporting a smart connect()ed component and a basic (non-redux) version of the same component in the same file. They then just don't test the redux-connected version at all. Please don't do this.

Avoiding testing your redux-connected components is a mistake for two reasons. The most obvious is that you're not testing the version of your component that your customers are going to use. This means you lose some confidence in your tests. You're explicitly leaving out important cases. The next reason is that the redux hooks API, which provides a vastly superior developer experience to connect() doesn't support this pattern. It's unlikely that you'll be able to continue to separate your component that way as you move into the future.

A better approach is to create some utilities that simplify the way you setup and render your components. I use three in my course: renderWithContext, getStoreWithState, and getStateWithItems. These utilities help me work with state and context without cluttering my tests with complex setup code.

Let's start with the most simple one getStoreWithState:

import { configureStore } from "@reduxjs/toolkit";

const reducer = { /* ... */ }

export const store = configureStore({ reducer });

export function getStoreWithState(preloadedState) {
  return configureStore({ reducer, preloadedState });
}

Redux Toolkit includes a configureStore method that allows you to preload it with state. The createStore method in redux includes this option as well. In the old days I would rely on tools like redux mock store to generate redux stores for testing, but you don't need it. You can generate a store for your tests that includes exactly the same reducers as your app, but also comes pre-loaded with whatever state you need for your tests.

The next utility that you'll need is a way to render your components with state and context. For my tests I'm usually using React Testing Library, but the same approach works fine if you're using enzyme.

import { render } from "@testing-library/react";

export function renderWithContext(element, state) {
  const store = getStoreWithState(state);
  const utils = render(
    <Provider store={store}>
      {element}
    </Provider>
  );
  return { store, ...utils };

I've seen a lot of test suites that include a mountWithStore function inside of them, but I think you get a ton of benefit moving this into an app-wide utility file. It makes it a lot easier to pre-populate state consistently and provide any additional context that might be needed for your tests.

With these two utilities in place it's fairly straight forward to render a component with arbitrary state pre-loaded.

import { renderWithContext } from "../test-utils";

test("error banner should appear", () => {
    renderWithContext(<Header />, { errors: [{ /* ...  */ } ] })
    expect(screen.getByRole("alert")).toHaveTextContent("Could not load data");
});

The only other utility I found to improve on this a little bit is one that generates the whole app state for you, but lets you modify some piece you might want. Some apps put this state in a JSON file, which can be helpful, but having a utility function that lets you override some common parts has proven crucial. This will always be unique to your app, but here's one example of what that could look like:

export function getStateWithErrors(errors) {
  const state = {
    products: { /* ... */ },
    cart: { checkoutState: "READY", items: {} },
    errors
  };
  return state;
}

With that the test above might be written like:

import {
  renderWithContext,
  getStateWithErrors
} from "../test-utils";

test("error banner should appear", () => {
    const state = getStateWithErrors([{ /* ... */ }]);
    renderWithContext(<Header />, state);
    expect(screen.getByRole("alert")).toHaveTextContent("Could not load data");
});
test("error banner should not appear", () => {
    const state = getStateWithErrors([]);
    renderWithContext(<Header />, state);
    expect((screen.queryByRole("alert"))).toBeNull();
});

With this approach you can imagine making it easy to generate state where you only need to pass in a single error message while the function takes care of the rest.

That's a bit about how utility functions have helped me write manageable and possibly enjoyable tests for my redux apps without having to resort to tricks that have made my tests less reliable. The next article in this series is Render Components with a Real Store.

If you want to learn more about my approach to testing redux applications please watch my course on egghead.io.

16