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;
};
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)
);
};
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 };
(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 asscreen
orfireEvent
) will come straight from @testing-library/react — exceptrender
, which has been replaced with our customrender
.
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),
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:
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 };
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();
});
Here are the steps in the custom render
method that make this work:
- The custom
render
method uses thepreloadedState
option (and thecreateStoreWithMiddlewares
function used in production) to create a new store. - The custom
render
method then creates a wrapper with a Redux Provider, passing the store with the preloaded state as a prop. - The custom
render
method uses the actual testing-library/reactrender
to render theui
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.
Top comments (2)
Hey Bonnie do you have a repo for this, I'm a bit confused on how to organize the files
Hi, Miguel,
I'm so sorry for the delayed response!! You can see an example of the file organization in this project in my GitHub repo. Happy to answer any follow-up questions!
Cheers,
Bonnie