← Go back
March 15, 2022 · 7 mins read min read

Redux Toolkit Testing Reducers

How to test reducers in Redux Toolkit with Jest and react-testing-library

redux numbers

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.

Tests with Redux

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.

Application

For that project we will use those technologies:

  • React as a UI Language
  • Redux as state manager
  • TailwindCss UI as our design toolkit
  • Redux Toolkit as a Data Flow Framework
  • Jest: A testing framework for JavaScript.
  • React Testing Library: A testing framework for React.

by the way I'm planning a post here to talk a little about Tailwindcss let me know if you would be interested

Project Structure

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

Testing Reducers

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:

testing 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.

Passing Tests

Recap

✨ Perfect ✨

  • our tests are working ✅
  • our reducer is responding correctly ✅
  • all our actions are being handled correctly ✅

so let's recap what we have learned so far:

  • when using RTK, we can create reducers and actions very easily
  • we can apply the isolation approach to organize our tests
  • we can use the functional 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.