mocking redux with react-testing-library

2021-05-28

 | 

~3 min read

 | 

549 words

In a previous post about abstracting utilities in Jest, I demonstrated how to create a wrapper for the render method of the React Testing Library (RTL).

Today, I’m going to revisit the topic to see how you can take the same approach but, instead of wrapping a theme around all of your components, use redux-mock-store to create a <Provider> wrapper enabling tests to access Redux. I’m also going to do it in Typescript which added a few wrinkles as it was not immediately obvious how to type the wrapper.

test/utils/rtl-wrapper.tsx
import * as React from "react"
import { render as rtlRender, RenderOptions } from "@testing-library/react"
import { Store } from "redux"
import { Provider } from "react-redux"
import thunk from "redux-thunk"
import configureStore from "redux-mock-store"
import { RootState } from "../src/app"

interface ExtendedRenderOptions extends RenderOptions {
    initialState: Partial<RootState>
    store?: Store<Partial<RootState>>
}

const render = (
    component: React.ReactElement,
    {
        initialState,
        store = configureStore<Partial<RootState>>([thunk])(initialState),
        ...renderOptions
    }: ExtendedRenderOptions = {
        initialState: {
            /* any default state you want */
        },
    },
) => {
    return rtlRender(component, {
        wrapper: TestWrapper(store),
        ...renderOptions,
    })
}

const TestWrapper = (store: Store) => ({
    children,
}: {
    children?: React.ReactNode
}) => <Provider store={store}>{children}</Provider>

export * from "@testing-library/react"
// override the built-in render with our own
export { render }

Let’s walk through what’s going on here.

ExtendedRenderOptions extends the RenderOptions from RTL to expect tests to provide a state object and possibly a store. Whenever render is called, we’re defaulting initialState to an empty object - but this is easily passed in the second parameter of render along with all of the other options.

Though the store could similarly be passed in, in this case, I’ve opted to default it to the store that I use throughout the rest of the application. The type of RootState is defined where I create the store in my application:

src/app.tsx
import * as React from "react"
import { createStore, applyMiddleware, compose } from "redux"
import thunk from "redux-thunk"
import { Provider } from "react-redux"
import { Content } from "./components"
import { combinedReducers } from "./store"

declare global {
    interface Window {
        __REDUX_DEVTOOLS_EXTENSION__: any
        __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any
    }
}

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

const store = createStore(
    combinedReducers,
    composeEnhancers(applyMiddleware(thunk)),
)
export type RootState = ReturnType<typeof store.getState>
export const App = () => {
    /*...*/
}

The store is what’s then passed along to the TestWrapper component, which is where I instantiate a redux store for my tests.

With this wrapper in place, it’s quite easy to test a component that expects a Redux store.

import * as React from "react"
import { MenuManagement } from "."

import { render } from "test/utils"

describe("it", () => {
    it("renders without crashing", () => {
        const navClickFunction = () => null
        render(<MenuManagement />, {
            initialState: { menu: { menuItem: [] } },
        })
    })
})

Note: I am treating my test utils as a module, which is how

In this case, <MenuManagement> expects a Redux store to be present and for there to a menu slice with menuItems in it, which is provided easily enough in the second parameter, the options object (this is our ExtendedRenderOptions from above).

And just like that, I have a nice, easy to use wrapper for my tests that “just works”!


Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!