DEV Community

Cover image for Getting Started with Redux and Testing Library
Bonnie Schulkin
Bonnie Schulkin

Posted on

Getting Started with Redux and Testing Library

Photo by Piret Ilver on Unsplash

If you’re reading this, I’m guessing you’re testing a Redux app with Testing Library. And you probably want some tests to start with the Redux store in a particular state as the initial testing conditions.

As you probably know, Testing Library emphasizes “testing behavior” (tests that interact with your app the way users would). Behavioral testing purists would say: to set up a Redux store with certain values, start the test by running through user interactions that populate the state.

However, that’s simply not practical to do for every test, especially if the desired state needs a lot of interactions (and possibly server values) for setup. This blog post details how to set up a store factory to generate a test store (with initial values) for test setup.

Creating a Store Factory

The idea here is, you have a “factory function” to create a new store. This function creates the store for both production and tests to make sure your tests are as close as possible to production code.

Examples

Here’s an example of a store factory function using Redux Toolkit and Redux Saga:

import {
  Action,
  configureStore,
  EnhancedStore,
  ThunkAction,
} from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";

export const createStoreWithMiddlewares = (
  initialState = {}
): EnhancedStore => {
  const sagaMiddleware = createSagaMiddleware();

  const store = configureStore({ YOUR REDUCERS HERE },
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware().prepend(sagaMiddleware).concat(YOUR MIDDLEWARES HERE),
    preloadedState: initialState,
  });

  sagaMiddleware.run(YOUR ROOT SAGA);

  return store;
};
Enter fullscreen mode Exit fullscreen mode

Here’s another one using Redux and Redux Thunk:

import { createStore, applyMiddleware, Store } from "redux";
import ReduxThunk from 'redux-thunk';

export const middlewares = [ReduxThunk];

export const createStoreWithMiddlewares = (initialState = {}): Store => {
  return createStore(
    YOUR REDUCERS HERE,
    initialState,
    applyMiddleware(...middlewares)
  );
};
Enter fullscreen mode Exit fullscreen mode

Both of these store factories have a createStoreWithMiddlewares function that takes an initialState and can be used to create either the production store or a test store — with the same configuration. You can use these examples to write a createStoreWithMiddlewares for your app.

Using the Store Factory: Production

The production store can be created using createStoreWithMiddlewares either in your store/index.js file or src/index.js, and added as the store prop to the Redux Provider.

It’s very important to add the Redux Provider in src/index.js and not in App.js! If App.js contains the Redux Provider with the production store, then you won’t be able to test the App component with your test store, since the actual production store will be used when you render <App />.

Using the Store Factory: Tests

Now that the production Redux Provider has been relegated to index.js, we have total control over the store for our tests. Follow these steps and delight in the power!

Step 1: Create a Custom Render Function

We can overwrite the Testing Library [render](https://testing-library.com/docs/react-testing-library/api#render) function with a custom render that includes a Redux Provider with a private store just for that test. Write this code in, say, src/test-utils/index.tsx (actual file location and name are not important. Also, if you’re not using Typescript, you’ll probably want to use index.jsx instead of index.tsx).

import { EnhancedStore } from "@reduxjs/toolkit"; // for redux-toolkit
// import { Store } from 'redux' // for non-toolkit
import {
  render as rtlRender,
  RenderOptions,
  RenderResult,
} from "@testing-library/react";
import { ReactElement, ReactNode } from "react";
import { Provider } from "react-redux";

import { configureStoreWithMiddlewares, RootState } from "../store";

type ReduxRenderOptions = {
  preloadedState?: RootState;
  store?: EnhancedStore; // for redux-toolkit
  // store?: Store // for non-toolkit
  renderOptions?: Omit<RenderOptions, "wrapper">;
};

function render(
  ui: ReactElement,
  {
    preloadedState = {},
    store = configureStoreWithMiddlewares(preloadedState),
    ...renderOptions
  }: ReduxRenderOptions = {}
): RenderResult {
  function Wrapper({ children }: { children?: ReactNode }): ReactElement {
    return <Provider store={store}>{children}</Provider>;
  }
  return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
}

// re-export everything
export * from "@testing-library/react";

// override render method
export { render };
Enter fullscreen mode Exit fullscreen mode

(this code is adapted from the Redux Testing Docs). Note that the Typescript is different for Redux-Toolkit than for plain Redux; use the lines that apply to your project (or no Typescript at all if that’s your jam).

The idea with the above code:

  • The custom render in this doc takes a preloadedState and UI component.
  • The custom render wraps the UI component in a Redux Provider, with a store that contains the preloadedState.
  • The code exports everything from @testing-library/react and then overrides the render method, so this file can be used in place of the actual @testing-library/react module (as we’ll see when we use it).
  • When importing from this file instead of @testing-library/react, all the methods except render (such as screen or fireEvent) will come straight from @testing-library/react — except render, which has been replaced with our custom render.

Note that you can create a store beforehand and pass it to the render function, or you can use the default, which is creating a new store with your preloadedState, using all of the configuration from the configureStoreWithMiddlewares function that our production uses:

    store = configureStoreWithMiddlewares(preloadedState),
Enter fullscreen mode Exit fullscreen mode

If you do create a store and pass it as an argument, it’s very important that a new store is created for every test (so that there’s no sharing of state between tests).

Step 2: Using custom render in tests

To use this custom render in a test, we’ll import from our test-utils/index.tsx file instead of from @testing-library/react.

Say you have a user profile page, that looks like this:

user profile web page showing user name and email

our web site has won numerous awards for our clean, spare style 😝

The UserProfile component might look something like this:

import { EnhancedStore } from "@reduxjs/toolkit"; // for redux-toolkit
// import { Store } from 'redux' // for non-toolkit
import {
  render as rtlRender,
  RenderOptions,
  RenderResult,
} from "@testing-library/react";
import { ReactElement, ReactNode } from "react";
import { Provider } from "react-redux";

import { configureStoreWithMiddlewares, RootState } from "../store";

type ReduxRenderOptions = {
  preloadedState?: RootState;
  store?: EnhancedStore; // for redux-toolkit
  // store?: Store // for non-toolkit
  renderOptions?: Omit<RenderOptions, "wrapper">;
};

function render(
  ui: ReactElement,
  {
    preloadedState = {},
    store = configureStoreWithMiddlewares(preloadedState),
    ...renderOptions
  }: ReduxRenderOptions = {}
): RenderResult {
  function Wrapper({ children }: { children?: ReactNode }): ReactElement {
    return <Provider store={store}>{children}</Provider>;
  }
  return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
}

// re-export everything
export * from "@testing-library/react";

// override render method
export { render };
Enter fullscreen mode Exit fullscreen mode

You can see that the user piece of state has name and email properties. To test that the user name and email show on the profile page, you need to preload the state with a user object for the test.

Here’s how the tests might look with our custom render method:

import { render, screen } from "../../test-utils"; // adjust for relative path to *your* test-utils directory 
import { UserProfile } from "./UserProfile";

const fakeUser = {
  name: "Tess Q. User",
  email: "tess-user@fake.domain.com",
};

test("User profile shows name and email", () => {
  render(<UserProfile />, { preloadedState: { user: fakeUser } });

  expect(screen.getByText("Tess Q. User")).toBeInTheDocument();
  expect(screen.getByText("tess-user@fake.domain.com")).toBeInTheDocument();
});
Enter fullscreen mode Exit fullscreen mode

Here are the steps in the custom render method that make this work:

  1. The custom render method uses the preloadedState option (and the createStoreWithMiddlewares function used in production) to create a new store.
  2. The custom render method then creates a wrapper with a Redux Provider, passing the store with the preloaded state as a prop.
  3. The custom render method uses the actual testing-library/react render to render the ui argument (in this case, <UserProfile />) wrapped in the newly-created Provider from step 2, and returns the result.

This result now has the store pre-populated with the specified user, the useSelector call in the component returns the fakeUser, and the tests pass.

Discussion (0)