Testing components with a request in rtk-query using msw and react-testing-library.
Hello everyone, I started testing a react web app and my requests for fetching and uploading data are made using rtk-query. I will guide you on how to write tests for components when using rtk query.
First, check out my tutorial on how to set up rtk query in redux toolkit.
You can use a library like msw to mock your api - that's what we use in the Redux Toolkit codebase to test RTK Query.
phryneas (Redux toolkit maintainer)
npm install msw --save-dev
To test RTK Query with react testing library? there are three steps,
- use
msw
to mock your API. - wrap your component in a real Redux store with your API.
- write your tests - use something to wait for UI changes.
Set up a custom render function
We need a custom render function to wrap our components when testing. This function is called renderWithProviders
To learn more
// ./src/test-utils.js
import React from 'react'
import { render } from '@testing-library/react'
import { Provider } from 'react-redux'
import { setupStore } from './app/store'
import { setupListeners } from '@reduxjs/toolkit/dist/query'
export function renderWithProviders(
ui,
{
preloadedState = {},
// Automatically create a store instance if no store was passed in
store = setupStore(preloadedState),
...renderOptions
} = {}
) {
setupListeners(store.dispatch);
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }
}
Redux store
we would set-up our redux store a little differently, for more info check here
// ./src/app/store.js
import { configureStore } from "@reduxjs/toolkit";
import { apiSlice } from "./api/apiSlice";
export const setupStore = preloadedState => {
return configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer,
},
preloadedState,
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
immutableCheck: false,
serializableCheck: false,
}).concat(apiSlice.middleware),
})
}
Provide the store to the App
We need to wrap our react app with the redux store we have set up
// ./src/index.js
import { setupStore } from './app/store'
import { Provider } from 'react-redux';
const store = setupStore({});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Set up test environment
Before we begin, we have to set up our test environment in JEST
setupTests.js
// ./src/setupTests.js
import '@testing-library/jest-dom';
import { server } from './mocks/api/server'
import { apiSlice } from './app/api/apiSlice'
import { setupStore } from './app/store'
const store = setupStore({});
// Establish API mocking before all tests.
beforeAll(() => {
server.listen();
});
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => {
server.resetHandlers();
// This is the solution to clear RTK Query cache after each test
store.dispatch(apiSlice.util.resetApiState());
});
// Clean up after the tests are finished.
afterAll(() => server.close());
We reset the API between the tests, as the API has internal state as well, by calling store.dispatch(apiSlice.util.resetApiState());
after each test
Mocking REST API
We use msw
to mimic (mock) the API request we make in our App. I will show you how to set up and use msw
.
msw
In your src
directory, create a folder mock
and a sub-folder api
API handler
The handler contains the global set up for a successful request, if the API was mocked (queried) successfully, the response will be taken from what we have defined in the msw
response object.
./src/mock/api/handler.js
// ./src/mock/api/handler.js
import { rest } from 'msw'
export const handlers = [
rest.get('https://jsonplaceholder.typicode.com/users', (req, res, ctx) => {
// successful response
return res(ctx.status(200), ctx.json([
{ id: 1, name: 'Xabi Alonzo' },
{ id: 2, name: 'Lionel Messi' },
{ id: 3, name: 'Lionel Love' },
{ id: 4, name: 'Lionel Poe' },
{ id: 5, name: 'Lionel Gink' },
]), ctx.delay(30))
})
]
./src/mock/api/server.js
// ./src/mock/api/server.js
import { setupServer } from 'msw/node'
import {handlers} from "./handler"
export const server = setupServer(...handlers)
Finally, writing tests
Test 1: Fetch from API
To handle a REST API request we need to specify its method, path, and a function that would return the mocked response. learn more.
This is our URL structure:
baseUrl: "https://api.coingecko.com/api/v3"
Query Parameters: ?vs_currency=ngn&order=market_cap_desc&per_page=100&page=1
The intercepted request is the request we make in our component.
const queryRequest = {
vs_currency: "usd",
order: "market_cap_desc",
per_page: "10",
sparkline: "false",
page
}
const {
data: coins,
isSuccess,
isError,
error,
isLoading
} = useGetCoinsQuery(queryRequest)
getCoins: builder.query({
query: (arg) => ({
url: `/coins/markets`,
params: {...arg}
}),
providesTags: ["coins"],
})
The test; fetching data from an API
// ./src/__test__/Crypto.test.js
const apiData = [
{name: "Mark Zuckerberg", age: "34"},
{name: "Elon Musk", age: "44"}
]
test("table should render after fetching from API depending on request Query parameters", async () => {
// custom msw server
server.use(
rest.get(`*`, (req, res, ctx) => {
const arg = req.url.searchParams.getAll("page");
console.log(arg)
return res(ctx.json(apiData))
}
)
);
// specify table as the render container
const table = document.createElement('table')
// wrap component with custom render function
const { container } = renderWithProviders(<Coins />, {
container: document.body.appendChild(table),
});
const allRows = await screen.findAllByRole("row")
await waitFor(() => {
expect(container).toBeInTheDocument();
})
await waitFor(() => {
expect(allRows.length).toBe(10);
})
})
explaining the test
- create a custom server :- For each test, we can over-ride the API handler to test individual sceneraios, by creating a custom
msw
server. -
req.url.searchParams.getAll
:- We use this to get all the query parameters that was sent with the request. - apiData :- this is the response we expect to be returned by the API.
- wrap table with container :- According to the RTL (react testing library) documentation, we need to specify table as the render container.
- wrap the component :- we wrap the component we want to test with our custom reder function.
- wildcard (*) :- We use this to represent the api URL.
- get all
tr
element :- I want to get alltr
element, so that I can check if we have up to 10 rows in the table. To do that I userow
, you can learn more here
Test 2: Mocking error responses
If you want to write test for an error sceneraio such as when the API server is unavailable.
The intercepted request
{isError && (<p data-testid="error" className="text-center text-danger">Oh no, there was an error {JSON.stringify(error.error)} </p>)}
{isError && (<p data-testid="customError" className="text-center text-danger">{error.data.message}</p>)}
The test; mocking error sceneraio
// ./src/__test__/Crypto.test.js
test('renders error message if API fails on page load', async () => {
server.use(
rest.get('*', (_req, res, ctx) =>
res.once(ctx.status(500), ctx.json({message: "baby, there was an error"}))
)
);
renderWithProviders(<Coins />);
const errorText = await screen.findByTestId("error");
const errorMessage = await screen.findByTestId("customError");
await waitFor(() => {
expect(errorMessage.textContent).toBe("baby, there was an error")
})
await waitFor(() => {
expect(errorText.textContent).toBe("Oh no, there was an error");
})
});
explaining the test
- create a custom server :- For each test, we can over-ride the API handler to test individual sceneraios, by creating a custom
msw
server. - we do not need the req
argument
because we are testing for error. - wrap the component :- we wrap the component we want to test with our custom reder function.
- wildcard (*) :- We use this to represent the API URL.
- res status code :- we intentionally throw an error with status code (500) to test for error.
- response body :- we pass in error message as an object to the response body.
Top comments (6)
Do you have a repo to check your code example? I'm having some problems to test an rtk query endpoint with a bearer token as a header
official redux toolkit testing docs
Hey thank you for taking the time to put this together, this is the first blog post (including the official docs) that has helped me to test my components properly.
The first link dev.to/masterifeanyi/how-to-setup-... is not working mate. Thank you
Thanks, I've been having a headache with this part.
Please donate