Mock API calls with MSW💡

·

5 min read

If you write React code that call API, for example like this:

const onSubmit = (data) => {
  axios
    .post(`${BASE_URL.API_BASE_URL}/api/v1/auth/register/`, data)
    .then(() => {
      toast.success(
        <span data-testid="toast-success">Successfully registered</span>,
      )
    })
    .catch((error) => {
      if (error.response.data.email) {
        toast.error(
          <span data-testid="toast-error">{error.response.data.email}</span>,
        )
      } else if (error.response.data.non_field_errors) {
        toast.error(
          <span data-testid="toast-error">
            {error.response.data.non_field_errors}
          </span>,
        )
      }
    })
}

You probably wonder, how do we suppose to test this? Is it okay if we test it directly to our backend API? You can do that, but there are concerns:

  • Our backend API may not be ready yet
  • The internet is slow so we cannot connect to it
  • and many others...

Calling backend API directly will also make the database full of our test data, which we obviously don't want. So, how do we solve this problem?

Introducing Mock

There is a term of Test Double that describes what we want to do. Test Double is basically an umbrella term. The specific method that we want to do is called Mock.

Taken from this well-explained source, here is the definition of Mock:

Mocks are objects that register calls they receive.

So basically, what we want to is to create a Mock object that will "register" API call. How do we verify that? We verify if the Mock object perform actions, and from the actions, we will verify the output of what we want to test is correct or not.

To make it simple, we will create a Mock object that will "call the API" and return the outputs. And from the output, we will assert something from that (based on code).

Basically, I am going to write a Mock object that will "call the API" and from the output, we are going to write test for three cases: success, error with email field, and error with non email field.

How to Mock in React

There are many options on mock in React. You can see many techniques from Kent C. Dodds's article. In our project, we use msw because it is very easy to understand and from the article that I have mentioned before, the approach is better than using jest.mock.

Let's try to implement for error test in email field. First, we need to write these line of codes:

import {rest} from 'msw'
import {setupServer} from 'msw/node'
import {fireEvent, screen, render} from '@testing-library/react'
import Register from './Register'
import userEvent from '@testing-library/user-event'

const server = setupServer(
  rest.post('http://localhost:8000/api/v1/auth/register/', (req, res, ctx) => {
    return res(ctx.status(400), ctx.json({email: 'Email exists'}))
  }),
)

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

Here is a little of bit explanation:

  • const server = setupServer(...): Basically defining which request that we are going to intercept. In the example above, I will intercept the API call of /register and make it return error with status code 400 and {email: 'Email exists'}
  • beforeAll(() => server.listen()): Before all test, we are going to define API mocking as mentioned above
  • afterEach(() => server.resetHandlers()): After each test, we are going to reset the URL to initial URL (see above)
  • afterAll(() => server.close()): After all tests are executed, we are going to close the server

Now, you can directly focus on test to show error toast show error toast if there is email error from backend

test('show error toast if there is email error from backend', async () => {
  render(<Register />)

  userEvent.type(screen.getByLabelText(/Email/i), 'sigendud@email.com')
  userEvent.type(screen.getByLabelText(/No Handphone Aktif/i), '8123456789')
  userEvent.type(screen.getByLabelText('Password*'), 'sebuahPassYangValid22')
  userEvent.type(
    screen.getByLabelText('Konfirmasi Password*'),
    'sebuahPassYangValid22',
  )

  const daftarButton = screen.getByRole('button')
  fireEvent.click(daftarButton)

  const toastError = await screen.findByTestId('toast-error')
  expect(toastError).toBeInTheDocument()
})

Since the msw will intercept the API call of register with email error, the test will surely pass. Hooray🥳

What about if we want to change API call with different response? You can override it every time you want to run the specific test. Confused? Take a look at this example.

Let's work on error case for non_field_errors. Previously, the API call will return status code 400 with response of {email: "Email exists"}. Now, we want to change it to return with response of {non_field_errors: "Password is too common}. We can override directly in our test block, like this:

test('show error toast if there is error (other than email) from backend', async () => {
  server.use(
    rest.post(
      'http://localhost:8000/api/v1/auth/register/',
      (req, res, ctx) => {
        return res(
          ctx.status(400),
          ctx.json({non_field_errors: 'Password is too common'}),
        )
      },
    ),
  )

  render(<Register />)

  userEvent.type(screen.getByLabelText(/Email/i), 'sigendud@email.com')
  userEvent.type(screen.getByLabelText(/No Handphone Aktif/i), '8123456789')
  userEvent.type(screen.getByLabelText('Password*'), 'password01')
  userEvent.type(screen.getByLabelText('Konfirmasi Password*'), 'password01')

  const daftarButton = screen.getByRole('button')
  fireEvent.click(daftarButton)

  const toastError = await screen.findByTestId('toast-error')
  expect(toastError).toBeInTheDocument()
})

Now, you can see how easy it is to use msw to mock API call.

Tips

  • msw is somewhat similar to NodeJS handler, so it must be very familiar to some of you😊
  • If you are not sure if which API call is not intercepted, you can do console too:
    server.listen({
    onUnhandledRequest(req) {
      console.error(
        'Found an unhandled %s request to %s',
        req.method,
        req.url.href,
      )
    },
    })
    
  • If you are working with form that will submit to backend API, make sure the form is already filled in the test! I was confused why the API is not called, it turned out that the form is actually not filled 😂. I change from fireEvent to userEvent library to handle why the form is not filled

Conclusion

First, you need to setup server with URL, request, and response. Don't forget to set listen, reset handlers, and close. Finally, focus on the detail of your test since API call will be intercepted.

Now, since you already master on mocking API call with msw, try to mock in my API's success case!

Â