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())
- 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
3. Tell Jest about the jestSetup configuration file by setting the setupFilesAfterEnv option.
<rootDir>/jest.config.js
module.exports = {
setupFilesAfterEnv: ['<rootDir>/jestSetup.js']
}
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))
})
]
<rootDir>/src/mocks/api/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
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
})
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)
})
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)
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:
You're testing code that's meant to run in a browser, so it relies on browser's globals like
fetch
orHeaders
orRequest
. 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!
Hey, I'm glad you liked this article and thanks for the suggestion. I updated the content
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.As you can see in the picture the
data
field isundefined
, and the status is alwayspending
, 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.
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.