DEV Community 👩‍💻👨‍💻

Cover image for How to test RTK Query with react testing library
Ifeanyi Chima
Ifeanyi Chima

Posted on • Updated on

How to test RTK Query with react testing library

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
Enter fullscreen mode Exit fullscreen mode

To test RTK Query with react testing library? there are three steps,

  1. use msw to mock your API.
  2. wrap your component in a real Redux store with your API.
  3. 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

Image description


// ./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 }) }
}

Enter fullscreen mode Exit fullscreen mode

Image description

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),
  })
}
Enter fullscreen mode Exit fullscreen mode

Image description

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>
);
Enter fullscreen mode Exit fullscreen mode

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());

Enter fullscreen mode Exit fullscreen mode

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

Image description

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))
  })
]
Enter fullscreen mode Exit fullscreen mode

./src/mock/api/server.js


// ./src/mock/api/server.js

import { setupServer } from 'msw/node'

import {handlers} from "./handler"

export const server = setupServer(...handlers)
Enter fullscreen mode Exit fullscreen mode

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"],
})
Enter fullscreen mode Exit fullscreen mode

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);
    })
})
Enter fullscreen mode Exit fullscreen mode

Buy Me A Coffee

explaining the test

  1. create a custom server :- For each test, we can over-ride the API handler to test individual sceneraios, by creating a custom msw server.
  2. req.url.searchParams.getAll :- We use this to get all the query parameters that was sent with the request.
  3. apiData :- this is the response we expect to be returned by the API.
  4. wrap table with container :- According to the RTL (react testing library) documentation, we need to specify table as the render container.
  5. wrap the component :- we wrap the component we want to test with our custom reder function.
  6. wildcard (*) :- We use this to represent the api URL.
  7. get all tr element :- I want to get all tr element, so that I can check if we have up to 10 rows in the table. To do that I use row, you can learn more here

Buy Me A Coffee

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>)} 
Enter fullscreen mode Exit fullscreen mode

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");
    })
});

Enter fullscreen mode Exit fullscreen mode

explaining the test

  1. create a custom server :- For each test, we can over-ride the API handler to test individual sceneraios, by creating a custom msw server.
  2. we do not need the req argument because we are testing for error.
  3. wrap the component :- we wrap the component we want to test with our custom reder function.
  4. wildcard (*) :- We use this to represent the API URL.
  5. res status code :- we intentionally throw an error with status code (500) to test for error.
  6. response body :- we pass in error message as an object to the response body.

reference

Testing components with a request for rtk-query

Testing React-query with MSW

Top comments (3)

Collapse
 
mikeypenny profile image
Miguel Sandoval

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

Collapse
 
ifeanyichima profile image
Ifeanyi Chima Author
Collapse
 
coder2012 profile image
Neil Brown

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.

Need a better mental model for async/await?

Check out this classic DEV post on the subject.

⭐️🎀 JavaScript Visualized: Promises & Async/Await

async await