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
.
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:
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!