Comprehensive guide to mocking - coursehero/ch-react-workshop GitHub Wiki

Motivation

First: when working on a new feature or simply prototyping, we want our UI components to get mock data that matches exactly what we expect from the server, and we want them to do so in the same way they would interface with a REST or GraphQL endpoint.

Second: when running front end tests, we want to avoid making actual network requests and instead mock the response. Developers usually approach this problem by mocking the underlying library used to make HTTP requests, such as axios and fetch, mocking the client, supplying additional props to determine if the component is being called from a test, running an E2E test specifically for this, or not testing anything at all. In many cases, these approaches can lead to code repetition and brittle tests that ultimately lower how confident we are in the code we are shipping.

How can we accomplish both of these as easily and seamlessly as possible, without having to rely on multiple external libraries, JSON servers, or complicated set-ups? Let’s find out!

Goals

In order to have a better development experience during development and testing, it would be great if we could:

  • Test our components with real data and network operations exactly as we would with actual endpoint(s)

  • Avoid having to mock axios, fetch, swr, react-query, apollo-graphql, etc

  • Avoid changing the implementation details of our component(s) to accommodate some specific test case(s)

  • Intercept the network requests our components make, provide a function to handle them, and return some mock data back to the component

  • Easily handle testing both success and failure cases for each request (different error codes, error messages, etc)

  • Reuse the same mock infrastructure and mock data for both development and testing

  • Reuse the same approach for both REST and GraphQL requests

  • To achieve all of the above, ideally we could do so with a relatively minimal set-up and low-maintenance cost

Enter Mock Service Worker

The utility functions and most snippets you’ll find in this document make use of the msw library under the hood. In essence, msw leverages Service Workers to intercept network requests and respond with the mock data you provide for that particular route/scenario. It is also the approach recommended by React Testing Library for what we want to accomplish here.

msw makes it possible for UI components to make network requests just as they would in production, but instead receive mock data that at the same time can be reused for testing.

Mocking an endpoint for development

Imagine you’re ready to start working on the UI for this new feature/product you got assigned, and you run into one or more of the following situations:

  • The back end developers haven’t finished (or even started) working on one or more of the endpoints you need and thus you can’t use them in your component yet – a very common case.

  • You’re prototyping something quickly, something for which there’s no existing endpoint, and all you want is to start displaying content in your UI and get unblocked.

  • You decided to take advantage of the company’s new benefit and are working from a cabin in the mountains for the next 6 months. It’s a really secluded place, real pretty and no need to worry about social distancing, but the catch is that the internet is almost dial-up slow or nonexistent at times.

  • For any of the above cases, you would normally stub some data via JSON files, a separate JSON server, or something similar. But this can easily lead to maintainability issues and throw-away code.

// users/components/Users.tsx

// The component we'll be using for mocks during tests and development.
// All it does is fetch a list of random users and renders them in the UI.
import * as React from 'react'
import { useState, useEffect } from 'react'
import axios from 'axios'

export const Users = () => {
  const [users, setUsers] = useState([])

  useEffect(() => {
    const fetchData = async () => {
      const { data } = await axios.get('https://randomuser.me/api/?results=3')

      setUsers(data?.results ?? [])
    }

    fetchData()
  }, [])

  if (users.length === 0) {
    return <div>No users available!</div>
  }

  return (
    <ul>
      {users.map(({ name, location }: any) => (
        <li key={name.first}>
          {name.first} - {location.city}
        </li>
      ))}
    </ul>
  )
}

MSW Request Handlers

Request Handlers have an array containing one mocked request for the endpoint that we care about, in this example, https://randomuser.me/api/, but there could be more for others we want to match based on URL, method, or other criteria. Under the hood when msw sees a request matching a resource for which there’s a request handler defined, it will intercept it and respond with the mock data that we passed to the res function. So now if we take a look at our network panel in the browser, we’ll see something like this:

As you can see, the request never actually made it out – the mock Service Worker caught it and handled it with the mock data we provided, which the Users component used to render to the UI. Just as easily, we could also add additional request handlers with different data to ensure our component is accounting for when the response is a 500, when the results array is empty, and so on.

It’s also worth mentioning that in the world of msw, there are two kinds of Request Handlers:

Initial Handlers: those passed to the setupWorker or setupServer functions initially. Think of them as: for a given module or app for which I have some endpoints that I want to mock, I’m going to have a set of initial request handlers watching those endpoints and responding with some default mock data.

Runtime Handlers: when working with Jest / RTL test files, runtime handlers are those that we define directly inside our individual test cases, which allow us to mock any endpoints we want and have those changes only run for the duration of that particular test.

Speaking of tests, can we reuse everything that we’ve done until this point? Yes we can, let’s take a look at that in the next section.

Testing a component with Jest and React Testing Library

In the React community, React Testing Library is the preferred technology for writing unit and integration tests. However, note that msw also supports E2E testing tools like Cypress. First, take a look at msw's set-up steps for tests.

Internally, msw starts a server (not a Service Worker like before) that will intercept network requests when we run our tests. This enables our tests to reuse the same mocking set-up that we laid out in the previous section for mocking API calls. This means there’s no need for us to have one set of logic for mocking network requests and a separate one for testing, an approach that would inevitably end up with overlaps and redundant code. We also don’t have to mock axios or fetch, our client, or run a specific E2E test for this either.

Now we’re going to add a test file for our Users component from the previous example:

Users.test.tsx
// users/components/Users.test.tsx

import * as React from 'react'
import { render } from '@testing-library/react'
import { server, rest } from 'utils/test-utils/msw/setupServer'
import { Users } from './Users'

// When the Users component mounts and calls the endpoint, our Initial Request Handler
// will intercept it and respond with the mock data we set.
test('Renders a list with the correct number of users', async () => {
  const { findByRole } = render(<Users />)
  const usersList = await findByRole('list')

  expect(usersList.children).toHaveLength(2)
})

// In this case, we're defining a Runtime Request Handler, which means that Users
// will instead be getting the mock data we set right here.
test('Renders placeholder test when no results are returned by the API', async () => {
  server.use(
    rest.get('https://randomuser.me/api/', (_req, res, ctx) => {
      return res(
        ctx.status(200),
        ctx.json({
          results: [],
        }),
      )
    }),
  )
  
  const { findByRole } = render(<Users />)
  const usersList = await findByRole('list')

  expect(usersList.children).toHaveLength(2)
})

Before each test in this file runs, Jest is going to look at setupTests.js and execute the code in it, causing our mock server to start. In the case of the first test, the Users component will receive the mock data set in our initial request handler, while the second one defined its own runtime handler with the purpose of mocking this endpoint differently only for this test.

Our tests above pass, and as expected if we make the following change to the first one, it will fail:

expect(usersList.children).toHaveLength(3)

Remember this line inside the component’s useEffect function:

axios.get('https://randomuser.me/api/?results=3')

Notice that we asked for 3 results, and with the latest change we made to the test, we are asserting that we have 3 results. But the reason the test fails is because our msw test server saw that we had a request handler for this URL and responded with our mock data instead, which had 2 user objects in it.

Benefits

  • Allows for rapid UI development and automated testing using the same underlying data and infrastructure.

  • The wrapper functions and set-up added help us ensure that all of our teams are interfacing with this mocking functionality in a consistent manner. It also makes it that minimal work is required from a developer to use this in their app.

  • Enables the developer to see how their components interact with the network just as they would with a real endpoint, and evaluate what their UIs look like and behave when there’s a success, error, or loading states just like the end-user would.

  • Encourages best practices for automated testing, namely not having to mock the underlying fetch libraries and mess with implementation details.

  • Flexible: it supports multiple fetch-like libraries, such as the native fetch, axios, swr, react-query, and others.

  • Future proof: this is the recommended approach for mocking API data in the React Testing Library documentation. It also supports GraphQL, which is something we’re going to be investing more on in the future.

  • In-production, includes documentation and how-to.

In summary

The process for both mocking network requests on the client and writing tests powered by the same infrastructure and mock data is as follows:

  • Write your React components' data-fetching logic as you normally would – use fetch, axios, react-query, SWR, take your pick.

  • Create a file for your module / app / section with your Initial Request Handlers. Here’s where you define the endpoints you’re interested in mocking and the mock data they’re going to be responding with.

  • If you want to test your components with Jest/RTL, just write your tests! If your Initial Request Handlers cover the cases you care about, there’s nothing else for you to do! But if you need to get specific, you can always define your own Runtime Request Handlers on a per-test-case basis.

Other related topics

Next steps

⚠️ **GitHub.com Fallback** ⚠️