28
Testing Redux toolkit in React / Nextjs application
In brief we will be just using
@reduxjs/toolkit
and we will be creating a customRender
function which lets you pass store data without mocking useSelector fromreact-redux
or any external libs
This article starts with a quick crash course on redux toolkit with respect to React. Then we also write test for the imaginary react component.
To have Redux to any react application, you need to wrap your root App component with Provider.
Below is common app.ts template in a Nextjs application
- Not having types for the sake of brevity
import { Provider } from 'react-redux'
import { store } from '../redux/store'
const App = ({ Component, pageProps }) => {
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
)
}
Now that we have basic Root App component we also gota have a Store which actually configure the Redux and reducers. aka createStore.
import { configureStore } from '@reduxjs/toolkit'
import { userSelector, userStateSlice } from './userStateSlice'
export const reducers = {
user: userStateSlice.reducer
}
// configureStore helps you createStore with less fuss
export const store = configureStore({
reducer: reducers
})
export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>
// e.g. to call thunk
// store.dispatch(loadUserAsync())
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { RootState } from './store'
import { getAuth } from '../auth/getAuth'
interface UserState {
loaded: boolean
apiHost?: string
username?: string
}
const initialState: UserState = { loaded: false }
export const userStateSlice = createSlice({
name: 'env',
initialState,
reducers: {
loadUser: (state, action: PayloadAction<UserState>) =>
(state = { ...action.payload, loaded: true })
}
})
// this internally uses Thunk
// store.dispatch(loadUserAsync())
export const loadUserAsync = () => dispatch =>
getAuth().then(user => dispatch(userStateSlice.actions.loadUser(user)))
export const userSelector = (state: RootState) => state.env
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import { AppDispatch, RootState } from './redux'
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispatch = () => useDispatch<AppDispatch>()
Now we are in our imaginary React / nextjs component
where we consume Redux store
import { useAppSelector } from '#src/redux/redux-hooks'
import { userSelector } from '#src/redux/userStateSlice'
...
const UserLandingPage = ({isAuthenticated}) => {
// useAppSelector is just a typescript wrapper around useSelector
const { user } = useAppSelector(userSelector)
useEffect(() => {
if (isAuthenticated && env?.apiHost) {
fetchUserOrders(env.apiHost)
}
}, [isAuthenticated, env])
return (
<ContentWrapper>
...
</ContentWrapper>
)
}
Now the main part, where we write boilerplate test for the above Component
import { renderWithStore } from '#test/render-with-store'
describe('<UserLandingPage>', () => {
const customInitialState = {
user: {
loaded: true,
apiHost: 'https://localhost:9000'
username: 'ajinkyax'
}
}
it('renders', async () => {
const { getAllByRole, getByText } = renderWithStore(<UserLandingPage {...props} />, customInitialState)
...
})
})
Now the meat of this testing is renderWithStore
which allows us to pass an initial store state and also prevents us from passing Provider to render. No more duplication of reducers for testing.
Also saves us from mocking useSelector
import { configureStore } from '@reduxjs/toolkit'
import { Provider } from 'react-redux'
import { render } from '@testing-library/react'
import { reducers, RootState } from '../src/redux/store'
const testStore = (state: Partial<RootState>) => {
return configureStore({
reducer: reducers,
preloadedState: state
})
}
export const renderWithStore = (component, initialState) => {
const Wrapper = ({ children }) => (
<Provider store={testStore(initialState)}>{children}</Provider>
)
return render(component, { wrapper: Wrapper })
}
Hope this was helpful, let me know in the comments if you get stuck anywhere.
28