DEV Community

Cover image for Mocking RTK Query API with Mock Service Worker for testing React Native Apps
JBudny
JBudny

Posted on • Edited on

Mocking RTK Query API with Mock Service Worker for testing React Native Apps

Today I will show you how I integrated the RTK Query RESTful API handler with Mock Service Worker for testing React Native project with Jest.

About the RTK Query

RTK Query (short RTKQ) is a tool shipped with the Redux Toolkit package designed for fetching and caching data.

What is Mock Service Worker and why should you use it?

Mock Service Worker (short MSW) is a library that allows you to intercept the actual requests at the highest level of the network communication chain and return mocked responses.

Mocking API with MSW lets you forget about request mocks inside the individual test files. It's relatively easy to set up and helps to keep your code more concise and clear.

Prerequisites

  • React Native project with RTKQ
  • node.js v16 or higher (MSW requirement)

Setup

1. Add necessary MSW package and fetch polyfills that React Native has not defined.

yarn add -D MSW cross-fetch abort-controller

2. Create the jestSetup.js file with the following configuration.

  • MSW configuration

<rootDir>/jestSetup.js

import { server } from './src/mocks/api/server'

// 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())
// Clean up after the tests are finished.
afterAll(() => server.close())
Enter fullscreen mode Exit fullscreen mode
  • Since Jest runs tests in the node.js environment, and we test the code meant to run in the browser, the browser globals like fetch / Headers / Request are not available there. We need to add these polyfills to fix it.

<rootDir>/jestSetup.js

import AbortController from 'abort-controller'
import { fetch, Headers, Request, Response } from 'cross-fetch'

global.fetch = fetch
global.Headers = Headers
global.Request = Request
global.Response = Response
global.AbortController = AbortController
Enter fullscreen mode Exit fullscreen mode

3. Tell Jest about the jestSetup configuration file by setting the setupFilesAfterEnv option.

<rootDir>/jest.config.js

module.exports = {
 setupFilesAfterEnv: ['<rootDir>/jestSetup.js']
}
Enter fullscreen mode Exit fullscreen mode

4. Define MSW handlers and server.

As you can see, you can use a wildcard character in the path.

<rootDir>/src/mocks/api/handlers.ts

import { rest } from 'msw'

export const handlers = [
rest.get(`${POKEMON_API_BASE_URL}/pokemon/*`, (req, res, ctx) => {
  return res(ctx.status(200), ctx.json(dummyPokemon))
 })
]
Enter fullscreen mode Exit fullscreen mode

<rootDir>/src/mocks/api/server.ts

import { setupServer } from 'msw/node'

import { handlers } from './handlers'

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

5. Change RTKQ cache behavior under the test environment to avoid following Jest warning:

Jest did not exit one second after the test run has completed.

<rootDir>/src/api/pokemonAPI.ts

export const api = createApi({
 keepUnusedDataFor: process.env.NODE_ENV === 'test' ? 0 : 60
})
Enter fullscreen mode Exit fullscreen mode

I also stumbled upon the following error while testing the custom useDebounce hook.

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

I fixed it by upgrading jest to ^27.5.1, @types/jest to ^27.4.1, and babel-jest to ^27.5.1.

And that's all! From now you can test all your components seamlessly with mocked API.

At the end an example test
<rootDir>/src/screens/HomeScreen/components/PokemonFinder/PokemonFinder.test.tsx

beforeEach(() => {
    jest.useFakeTimers()
})

afterEach(() => {
    jest.useRealTimers()
})

test('PokemonFinder should display image with proper source', async () => {
 const store = setupStore()
 const { findByTestId, getByPlaceholderText } = 
 renderWithProviders(
  <PokemonFinder />,
  { store }
 )
 const searchInput = getByPlaceholderText(placeholder)
 fireEvent.changeText(searchInput, name)
 act(() => {
  jest.advanceTimersByTime(delay)
 })
 const image = await findByTestId('official-artwork')
 expect(image.props.source.uri).toBe(sprite)
})
Enter fullscreen mode Exit fullscreen mode

Hope you find this article useful 😃

You can see the full Github repository here

Conclusion

Mock Service Worker proves that mocking requests does not need to occur all over your tests. It offers a little more concise code for a reasonable amount of boilerplate. As a result, it makes our environment friendlier.

Acknowledgments

I decided to try MSW with React Native after reading this article.
Thanks to Kent C. Dodds for stop-mocking-fetch

Top comments (4)

Collapse
 
kettanaito profile image
Artem Zakharchenko

Hey, Jakub! Thank you for writing this pieace on using RTK Query and MSW!
There's but one thing I may suggest: split the setup step into polyfills and MSW setup. You add the following polyfills not because MSW needs them, but because your test environment needs them:

global.fetch = fetch
global.Headers = Headers
global.Request = Request
global.Response = Response
global.AbortController = AbortController
Enter fullscreen mode Exit fullscreen mode

You're testing code that's meant to run in a browser, so it relies on browser's globals like fetch or Headers or Request. That is why you add polyfills.

Such separation in the same testing setup step is essential for people to better understand what they're adding to the setup and why.

Regardless, I've enjoyed reading this one!

Collapse
 
jbudny profile image
JBudny

Hey, I'm glad you liked this article and thanks for the suggestion. I updated the content

Collapse
 
gcdcoder profile image
Gustavo Castillo • Edited

Great article! I have a question though, how could we change the response returned by the RTK query? e.g I need to test the different values returned by the query (isLoading, error, data, and so on) in the first render, but I don't know how to put some custom values in the response.

Image description

As you can see in the picture the data field is undefined, and the status is always pending, is there a way to change these values as need it?

MSW is intersecting the request correctly, but it seems that the data we put as response is not being taking into account for the RTK query.

Collapse
 
gcdcoder profile image
Gustavo Castillo

After a while I figured it out, it turns out I wasn't using the waitFor function from @testing-library/react 🤦🏽‍♂️. If anyone is interested in seeing a working example, you can check this repo.