Hello 👋

I'm Alejandro

A Web Engineer from Málaga, Spain based in London, UK

Mocking HTTP requests with Mock Service Worker

There is a lot of talk these days about if when you are testing your API integration you should be mocking your client (fetch/axios...etc.) or not.

To answer your question, we want our tests to be as reliable as possible, so if they work as closes as they will do in real life, that will be the ideal.

In this post, I'm going to show you how you can mock your server so it responds with the data you are expecting, instead of mocking your client.

But... will that make my test slow? because it will have to wait for the api response to come back? That's when MSW (Mock Service Worker) comes to the rescue.

What is Mock Service Worker?

As its own name highlights, this package is a Service Worker, and one of the things that a Service Worker does pretty well, is to intercept HTTP requests.

So, all it's! A server that intercept HTTP requests and respond with the data you tell it to respond with.

Let's see it in practice

Before MSW, all you will do to expect your UI to reflect what your api responds with, is to mock your client. Let's try with axios.

import * as React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import axios, { AxiosResponse } from 'axios'

jest.mock('axios')

test('CheckoutForm submits successfully', async () => {
  axios.post.mockResolvedValueOnce({ status: 200 } as AxiosResponse)

  render(<CheckoutForm />)

  userEvent.type(screen.getByLabelText(/account holder/i), 'Test User')
  userEvent.type(screen.getByLabeltext(/card number/i), '2221000000000009')
  userEvent.type(screen.getByLabeltext(/expiry date/i), '10-23')
  userEvent.type(screen.getByLabeltext(/cvv/i), '123')

  userEvent.click(screen.getByText(/pay/i))

  expect(await screen.findByText(/payment successful/i))
})

In this case, axios will respond with status 200 and will make our UI show the "Payment Successful" message.

Now, one thing that this sort of test suffers a lot from, is that you need to redo this mocking every time you need a new test, which makes a lot of duplication of the same mock response, which then needs to be change in every single place if the backend changes.

What if mocking is done only in one place?

Install msw

yarn add msw --dev

This package, exports:

  • setupWorker, which creates a service worker.
  • rest, which is to handle rest endpoints.
  • graphql, which is to handle graphql requests.

Create a file for your server handlers:

// handlers.js
import { rest } from 'msw'
import auth from '../core/services/auth'
import payments from '../core/services/payments'

export const handlers = [
  rest.get('/checkout', async (req, res, ctx) => {
    const isAuthorized = auth.authorize(req.headers.Authorization)

    if (!isAuthorized) {
      return res(ctx.status(401), ctx.json({ message: 'Not authorized' }))
    }

    const response = await payments.checkout(req.paymentDetails)

    if (!response.ok) {
      return res(ctx.status(400), ctx.json({ message: 'Payment Failed' }))
    }

    return res(ctx.status(200))
  }),
]

Configure the server

Because jest is running in node, we need to use the node api from msw, but don't worry msw is already taking care of all that.

The awesome thing about msw, is that you can use it as a fake development server as well, so if the endpoints you need to use are not ready yet, you can intercept that request and send back your test data.

// server.js
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

const server = serverSetup(...handlers)

beforeAll(() => server.listen())

afterEach(() => server.resetHandlers())
afterAll(() => server.close())

Add the server file to your jest configuration

// jest.config.js
module.exports = {
  ...
  setupFilesAfterEnv: [
    '@testing-library/jest-dom/extend-expect',
    './server'
  ]
  ...
}

Ready to go!

Now our test will look like this:

import * as React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('CheckoutForm submits successfully', async () => {
  render(<CheckoutForm />)

  userEvent.type(screen.getByLabelText(/account holder/i), 'Test User')
  userEvent.type(screen.getByLabeltext(/card number/i), '2221000000000009')
  userEvent.type(screen.getByLabeltext(/expiry date/i), '10-23')
  userEvent.type(screen.getByLabeltext(/cvv/i), '123')

  userEvent.click(screen.getByText(/pay/i))

  expect(await screen.findByText(/payment successful/i))
})

Exclusively caring about what you should be testing, leaving apart the implementation details of the fetch response and any other backend configuration (like headers, properties...etc.)

And you can reuse all the handlers, per tests and also in your development process.

If you want to have a deeper look at msw, check out their documentation page.

Share this post

Read more about