Mock Service Worker

Mock Service Worker (MSW) is a powerful tool designed to intercept requests on the network level. MSW allows us to reuse the same mock definition for testing, development, and debugging.

Mock Service Worker is an API mocking library that uses Service Worker API to intercept actual requests.

In this demo, we'll be configuring MSW to a Create React App testing environment and reusing the same network request mock definitions for testing and development.

GitHub repo


Contents


Create React App (CRA) uses Jest as a test runner and React Testing Library for testing react components. (Sep 2020) - There is an open issue with CRA version of JSDOM. If you run into this issue. Upgrade JSDOM:

npm i jest-environment-jsdom-sixteen
// package.json "test": "react-scripts test --env=jest-environment-jsdom-sixteen"

Install MSW

In your project directory run:

npm install msw --save-dev

Define mocks

Keep API mocking-related modules in a single directory (recommended).

Create a mocks directory:

mkdir src/mocks

Create a request handler file to handle our API requests

touch src/mocks/handlers.js

Import MSW to mock REST API requests. (MSW also supports GraphQL)

// handlers.js import { rest } from "msw"

We can Mock our API requests with MSW rest. Each request method in this namespace represents a REST API:

rest.get(), rest.post(), rest.put(), rest.patch(), rest.delete(), rest.options().

Next, we need to create a response resolver function to capture the request and return a mocked response. The response resolver function accepts the following parameters:

  • req, Request: information about a matching request.
  • res, Response: function to create the mocked response.
  • ctx, Context: Context utilities specific to the current request handler.

Example of a request handler and response resolver function

// handlers.js import { rest } from "msw" // Must match real URL (won't be called) const issuesURL = "https://api.github.com/repos/facebook/react/issues" // Mocked response const issues = [ { title: "Bind event to react hook component.", number: 19823, }, { title: "Enable source maps for DevTools.", number: 19773, }, ] // Request handler const handlers = [ // Response resolver function rest.get(issuesURL, (req, res, ctx) => { return res(ctx.status(200), ctx.json(issues)) }), ] /* Export rest to add additional request handlers to this instance. */ export { handlers, rest, issuesURL }

Setup Server

setupServer() sets up the request interception layer. We must pass our request handlers previously defined in handlers.js.

// App.test.js import { handlers } from "./mocks/handlers" /* Configure setupServer with the request handlers previously defined. */ const server = setupServer(...handlers)

Start testing

// App.test.js import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { setupServer } from 'msw/node'; import App from './App'; import { handlers, rest, issuesURL } from './mocks/handlers'; const server = setupServer(...handlers); /* server.listen() Establishes a request interception instance before all tests. */ beforeAll(() => server.listen()); /* server.close() Stops and cleans up request interception, preventing this interception from affecting all irrelevant tests. */ afterAll(() => server.close()); /* server.resetHandlers() Removes request handlers added in runtime. */ afterEach(() => server.resetHandlers()); // Mock description. test('Renders open issues', async () => { /* render returns an object with queries from DOM testing library. */ const { getByText } = render(<App />); // Assert loading state. expect(getByText(/Loading.../i)).toBeInTheDocument(); /* `waitFor` method can be useful to wait for an element to appear or disappear in response of an action. */ await waitFor(() => { // Assert two ticket numbers. expect(getByText(/19773/i)).toBeInTheDocument(); expect(getByText(/19773/i)).toBeInTheDocument(); }); }); // Mock description. test('Fails fetching open issues', async () => { server.use( // rest instance from handlers.js rest.get(issuesURL, (req, res, ctx) => { // return 400 status code to simulate a bad request. return res(ctx.status(400)); }) ); const { getByText } = render(<App />); // Wait for response await waitFor(() => { // Assert error message. expect(getByText(/Something went wrong./i)).toBeInTheDocument(); }); }); // Mock description. test('Zero opened issues', async () => { server.use( rest.get(issuesURL, (req, res, ctx) => { // return empty object return res(ctx.status(200), ctx.json({})); }) ); const { getByText } = render(<App />); await waitFor(() => { // Assert zero issues message expect(getByText(/There are no open issues./i)).toBeInTheDocument(); }

App.js

In App.js we have a React component fetching a request list of React's open issues from GitHub. The component handles loading, error, and success state. In addition to a zero issues message.

// App.js import React, { useState, useEffect } from "react" const fetchIssues = async () => { const url = "https://api.github.com/repos/facebook/react/issues" const resp = await fetch(url) if (!resp.ok) { throw new Error(`Req failed: ${resp.status}`) } return resp.json() } const App = () => { const [issues, setIssues] = useState([]) const [status, setStatus] = useState("loading") useEffect(() => { fetchIssues() .then(resp => { setIssues(resp) setStatus("success") }) .catch(() => { setStatus("error") }) }, []) if (status === "loading") { return <p>loading...</p> } if (status === "error") { return <p>Something went wrong.</p> } return ( <ul> {issues.length > 1 ? ( issues.map(issue => ( <li key={issue.number}> #{issue.number} {issue.title} </li> )) ) : ( <li>There are no open issues.</li> )} </ul> ) } export default App

By now, we should be able to see 3 passing tests by running:

npm test

Setup worker

The service worker will intercept our requests and allow us to use the mock API during development. The service worker needs to be in the public directory of your project.

In your project directory run:

//npx msw init <PUBLIC_DIR> npx msw init public

Configure the worker

Create a file for our worker's instance

touch src/mocks/browser.js

In the browser.js create a service worker instance with our request handlers.

// browser.js import { setupWorker } from "msw" import { handlers } from "./handlers" /* This configures a request mocking server with the given request handlers. */ export const worker = setupWorker(...handlers)

Start worker

The worker needs to be imported into our app's code in order to execute during runtime. Make sure this worker only runs in development.

App.js

if (process.env.NODE_ENV === "development") { const { worker } = require("./mocks/browser") //Registers and activates the Service Worker worker.start() }

By now our setupWorker should be using our Mock definitions on our application code. If CRA is not running; in a new terminal window run:

npm start

Verify a successful activation by opening the browser's DevTools console and you should see a verification message from MSW:

[MSW] Mocking enabled

Last, go to the DevTools Network tab. Our mock response should be listed😃. Now, we can very easily interact with our request handlers by making quick changes on our resolver functions and testing all possible scenarios with ease.