Skip to content

How to test Redux Toolkit Query with MSW

Posted on:February 11, 2023 at 02:45 PM

Table of Contents

Open Table of Contents

Introduction

When we work with web-based applications, it’s common to fetch data via HTTP calls. There are several options to do so, for example, react-query, axios, swr, and many others. In this article, we’ll see how to make HTTP calls using RTK Query (a similar library to react-query) and how to test it using MSW.

No time to read this post? no problem, here is the link to the repo 🧑🏽‍💻.

What is MSW?

First, let’s define what is MSW (Mocke Service Worker), according to its documentation:

Mock by intercepting requests on the network level. Seamlessly reuse the same mock definition for testing, development, and debugging.

Setting up MSW

To use msw, we need to install it as a dev dependency of our project. In my case, I’m using pnpm as my package manager.

pnpm add -D msw

Then, let’s set up the server using Jest as follows:

import '@testing-library/jest-dom'
import { fetch, Headers, Request, Response } from 'cross-fetch'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import dummyPokemon from 'src/tests/dummyPokemon'

global.fetch = fetch
global.Headers = Headers
global.Request = Request
global.Response = Response

export const handlers = [
  rest.get('https://pokeapi.co/api/v2/pokemon/*', (_req, res, ctx) => {
    return res(ctx.json(dummyPokemon))
  }),
]

export const server = setupServer(...handlers)

// Enable the API mocking before tests.
beforeAll(() => server.listen())

// Reset any runtime request handlers we may add during the tests.
afterEach(() => server.resetHandlers())

// Disable the API mocking after the tests finished.
afterAll(() => server.close())

Make sure to insert this code inside the jestSetup.ts file to make it work.

Setting up the Redux store

Now, let’s configure the redux store and the query example as follows:

import { configureStore, PreloadedState, combineReducers } from '@reduxjs/toolkit'
import counterReducer from 'src/store/slices/counter'
import { pokemonApi } from 'src/services'
import { setupListeners } from '@reduxjs/toolkit/query'

const rootReducer = combineReducers({
  counter: counterReducer,
  [pokemonApi.reducerPath]: pokemonApi.reducer,
})

export const setupStore = (preloadedState?: PreloadedState<RootState>) =>
  configureStore({
    reducer: rootReducer,
    preloadedState,
    middleware: getDefaultMiddleware => getDefaultMiddleware().concat(pokemonApi.middleware),
  })

const store = setupStore()

setupListeners(store.dispatch)
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof rootReducer>
export type AppStore = ReturnType<typeof setupStore>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = AppStore['dispatch']

Note: I’m using a counter slice in this code. However, it’s not necessary for this demo (feel free to remove it if you want.)

Setting up the service

To make the HTTP call to the Pokemon API, we should create a query that receives the Pokemon name as query param. Let’s see how it works:

// Need to use the React-specific entry point to import createApi
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
  reducerPath: 'pokemonApi',
  keepUnusedDataFor: process.env.NODE_ENV === 'test' ? 0 : 60,
  baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
  endpoints: builder => ({
    getPokemonByName: builder.query<any, string>({
      query: name => `pokemon/${name}`,
    }),
  }),
})

// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetPokemonByNameQuery } = pokemonApi

Implementing the App component

Once we have the service is the moment to implement the App. Let’s add the following code to the App.tsx file:

import { useGetPokemonByNameQuery } from 'src/services'

function App() {
  const pokemonQuery = useGetPokemonByNameQuery('pikachu')

  if (pokemonQuery.isLoading) {
    return <div>Loading...</div>
  }

  return (
    <div>
      {pokemonQuery.error ? (
        <>Oh no, there was an error</>
      ) : pokemonQuery.isLoading ? (
        <>Loading...</>
      ) : pokemonQuery.data ? (
        <>
          <h3 style={{ textAlign: 'center' }}>{pokemonQuery.data.species.name}</h3>
          <img src={pokemonQuery.data.sprites.front_shiny} alt={pokemonQuery.data.species.name} />
        </>
      ) : null}
    </div>
  )
}

export default App

Implementing the test

After we setup msw and rtk query is time to implement our test. Let’s add the following code to the App.test.tsx file:

import { waitFor } from '@testing-library/react'
import { renderWithProviders } from 'src/testUtils'
import App from 'src/App'
import { server } from 'src/jestSetup'
import dummyPokemon from 'src/tests/dummyPokemon'
import { rest } from 'msw'

describe('App', function () {
  test('PokemonFinder should display image with proper source', async () => {
    const { getByText, getByAltText, queryByText } = renderWithProviders(<App />)

    expect(queryByText(/loading.../i)).toBeInTheDocument()

    await waitFor(() => {
      const rgxName = new RegExp(dummyPokemon.name, 'i')
      const rgxAlt = new RegExp(dummyPokemon.species.name, 'i')
      expect(getByText(rgxName)).toBeInTheDocument()
      expect(getByAltText(rgxAlt)).toBeInTheDocument()
    })

    expect(queryByText(/loading.../i)).not.toBeInTheDocument()
  })

  test('PokemonFinder should display an error when the request fail', async () => {
    server.use(
      rest.get('https://pokeapi.co/api/v2/pokemon/*', (_req, res, ctx) => {
        return res(ctx.status(500), ctx.json('an error has occurred'))
      }),
    )

    const { getByText, queryByText } = renderWithProviders(<App />)

    expect(queryByText(/loading.../i)).toBeInTheDocument()

    await waitFor(() => {
      expect(getByText(/oh no, there was an error/i)).toBeInTheDocument()
    })

    expect(queryByText(/loading.../i)).not.toBeInTheDocument()
  })
})

Note: the mock value for the dummyPokemon is big. You can find the whole object here.

Running the test

Finally, the moment of truth (🥁):

Executing the test in the shell

Conclusion

And there you have it!. You can play more with the complete code by clicking on the following link: