How to test reducers in Redux Toolkit with Jest and react-testing-library
I was reading some articles about the 'redux evolution', and it makes me happy to see that in 2022 redux remains quite relevant, and not only relevant also still hitting download numbers in the house of 7,000,000 per week.
Being so widespread and so popular, you might think that until now, no one would have had a hard time testing redux/RTK apps, but as I was doing some research with the developer community I realised that a lot of people still don't know how to write tests for cases like reducers
, asynchronous actions
, selectors
, middlewares
, etc.
Taking advantage of this and also the fact that many people asked me to make a post about tests with Redux ToolKit (RTK)
, I decided to launch my new blog✨ and talk a little about this subject.
Whoever has already come from an application with react-redux
, must already be very familiar with the benefits of taking a functional approach and testing your application in "isolation" testing reducers and action creators for example.
Let's think of a simple Airbnb-style application where we will have a page with the list of results, where we can filter them by name, and favorites.
For that project we will use those technologies:
by the way I'm planning a post here to talk a little about
Tailwindcss
let me know if you would be interested
You can find a link to the project repository here please feel to browse, it will make it easier to follow this post and if you have any questions or suggestions feel free to contact me.
The project structure is as follows:
src
├── App.tsx // the main component
├── app
│ ├── hooks.ts
│ └── store.ts
├── components // general components
│ ├── HeartIcon.tsx
│ ├── Map.tsx
│ ├── Search.tsx
│ └── SearchIcon.tsx
├── features
│ └── home // the home page module
│ ├── Home.spec.tsx // integration tests
│ ├── Home.tsx
│ ├── HomeSlice.spec.ts // tests for the slice
│ ├── HomeSlice.ts
│ ├── Item.tsx
│ ├── homeAPI.ts
│ └── types.ts
├── mocks // mock data for the tests
│ └── results.ts
├── setupTests.ts
└── test-utils.jsx // utils for the tests
We can start with the functionality of filtering results
, add favorites
, and remove favorites
, as we are using RTK to simplify the creation of reducers and action creators let's create a slice for the HOME entity with this, we will define the structure in our reducer, and add our actions,
import { createSlice } from '@reduxjs/toolkit'
export interface HomeState {
query: string
results: Results[]
favorites: number[]
status: 'idle' | 'loading' | 'failed'
}
const initialState: HomeState = {
query: '',
results: [],
favorites: [],
status: 'idle',
}
export const homeSlice = createSlice({
name: 'home',
initialState,
reducers: {
setSearchQuery: (state, action) => {
state.query = action.payload
},
addFavorite: (state, action) => {
state.favorites.push(action.payload)
},
removeFavorite: (state, action) => {
state.favorites = state.favorites.filter((id) => id !== action.payload)
},
},
})
export const { setSearchQuery, addFavorite, removeFavorite } = homeSlice.actions
export default homeSlice.reducer
Now let's create our test file as I said early one good advantage of Redux is that our reducers are pure functions and because of that we can easily test them in isolation.
Let's then divide our test into three basic steps:
So our test will look like this:
import homeReducer, {
addFavorite,
HomeState,
removeFavorite,
setSearchQuery,
} from './HomeSlice'
describe('Home reducer', () => {
const state: HomeState = {
query: '',
results: [],
favorites: [],
status: 'idle',
}
it('should handle initial state', () => {
const initialState: HomeState = state
const action = { type: 'unknown' }
const expectedState = initialState
expect(homeReducer(initialState, action)).toEqual(expectedState)
})
it('should handle setSearchQuery', () => {
const initialState: HomeState = { ...state, query: '' }
const action = setSearchQuery('test')
const expectedState: HomeState = { ...state, query: 'test' }
expect(homeReducer(initialState, action)).toEqual(expectedState)
})
it('should add to favorites', () => {
const initialState: HomeState = { ...state, favorites: [] }
const action = addFavorite(1)
const expectedState: HomeState = { ...state, favorites: [1] }
expect(homeReducer(initialState, action)).toEqual(expectedState)
})
it('should remove from favorites', () => {
const initialState: HomeState = { ...state, favorites: [1] }
const action = removeFavorite(1)
const expectedState: HomeState = { ...state, favorites: [] }
expect(homeReducer(initialState, action)).toEqual(expectedState)
})
})
Now let's check the terminal to see if our tests are passing, if they are let's move on to the next step.
✨ Perfect ✨
so let's recap what we have learned so far:
isolation
approach to organize our testsfunctional programming mindset
and test our reducers with following pattern:Initial State => Action => Expected State
And if you made it this far, my heartfelt thanks 💛 help me by sharing this post and tell me your opinion here, my goal is to share and learn so don't forget to share and follow me on social media.