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 (🥁):
Conclusion
And there you have it!. You can play more with the complete code by clicking on the following link: