Mock GraphQL and REST in Storybook and Jest with MSW

This article is long (and dry), but at the end, you will have a setup that enables mocking GraphQL or REST responses at network level, and sharing the mocks between Storybook and Jest tests.

Storybook is really good for developing components in isolation, especially for presentational (or “dumb”) components which are purely driven by their props (or their contexts). However, some “smart” components will need to fetch data via GraphQL or REST APIs. These components might also have UI treatment for loading and error states, depending on server responses.

To achieve the development-in-isolation goal, these server-side responses need to be mocked, ideally without any server.

One common approach is to use library-specific tools such as Apollo’s MockedProvider or Axio’s mock adapter to mock server response. These tools are often easy to setup and use. The downside, however, is that they are tied to specific data-fetching libraries. When you need to swap out these libraries, for example, migrating ApolloClient to React-Query, your mock tools will have to be changed too.

MSW

Mock Service Worker (MSW) is a generic mocking tool that is not tied to any data-fetching library. Service Worker is an industry standard that is designed to intercept HTTP requests and override responses. With MSW, you can mock HTTP response in both browser (for Storybook) and Node (for Jest) environment.

Here is a screenshot showing how to mock responses for a GraphQL query (Persons) and mutation (UpdatePerson) in Storybook:

MSW mocks for GraphQL queries and mutations in Storybook

In browser, HTTP requests will show up in the Network panel even when they are mocked, because requests are actually initiated but got intercepted and mocked by Service Worker.

Mocked requests show up in browser’s dev tools

OK, but how do we integrate MSW with Storybook and Jest?

Install

First, install MSW:

npm install msw --save-dev

Then, generate a mock service worker script in a public directory. Run the following command at your project root:

npx msw init <PUBLIC_DIR> --save

<PUBLIC_DIR> is a directory that serves public static assets such as images. You can find the public directory for your project in this list: https://mswjs.io/docs/getting-started/integrate/browser#where-is-my-public-directory. If you don’t see a match in the list, don’t worry, just use public/for now. The generated script (mockServiceWorker.js) will be put in the “public” directory at project root.

The generated script should be committed to Git. Make sure you don’t git ignore this script. No need to change anything in the script.

After you run the above npx command, a new entry will be added to package.json:

// package.json"msw": {
"workerDirectory": "public"
}

This is for MSW to track the public directory in future.

Integration with Storybook

In the same package.json file where you defined the Storybook commands, add -s ./public to the commands, so Storybook will include the “public/mockServiceWorker.js” file when serving stories. For example:

// package.json"scripts: {
"storybook": "start-storybook -s ./public",
"build": "build-storybook -s ./public"
}

Now, create a msw-utils.js file at your project root (the filename can be anything, but for this article, let’s just name it msw-utils.js):

// msw-utils.jsimport { setupWorker } from "msw";
import { setupServer } from "msw/node";
function setup() {
let worker;
let server;
if (typeof global.process === "undefined") {
// Setup mock service worker for browser environment
worker = setupWorker();
} else {
// Setup mock service server for node environment
server = setupServer();
}
return { worker, server };
}
export const { worker, server } = setup();
export const mock = (worker || server).use;
export { graphql, rest } from "msw";

Optionally, if you use TypeScript, create a msw-utils.ts instead like this:

// msw-utils.tsimport { SetupWorkerApi, setupWorker } from "msw";
import { SetupServerApi, setupServer } from "msw/node";
function setup() {
let worker: SetupWorkerApi | undefined;
let server: SetupServerApi | undefined;
if (typeof global.process === "undefined") {
// Setup mock service worker for browser environment
worker = setupWorker();
} else {
// Setup mock service server for node environment
server = setupServer();
}
return { worker, server };
}
export const { worker, server } = setup();
export const mock = (worker ?? server)!.use;
export { graphql, rest } from "msw";

Note: there won’t be any server running even if setupServer indicates so.

In .storybook/preview.js (create if it doesn’t exist), we start the Mock Service Worker for Storybook:

// .storybook/preview.jsimport { worker } from "../msw-utils";const MSW_FILE = "mockServiceWorker.js";// In browser, start Mock Service Worker to mock HTTP responses
worker.start({
serviceWorker: { url: `./${MSW_FILE}` },
// Return the first registered service worker found with the name
// of `mockServiceWorker`, disregarding all other parts of the URL
findWorker: scriptURL => scriptURL.includes(MSW_FILE),
onUnhandledRequest: "bypass"
});

Here, we import and start the worker with the following configurations:

  • serviceWorker — This is to tell MSW where to find the generated script “mockServiceWorker.js”.
  • findWorker — This is to use the first found worker with the same file path.
  • onUnhandledRequest — This configuration disables warnings in browser console for un-mocked requests.

At this point, your project should have a file structure like this:

your-project
|
+ .storybook
| |
| + preview.js
|
+ public
| |
| + mockServiceWorker.js // Generated script
|
+ src
|
+ msw-utils.js // or msw-utils.ts
|
+ package.json

With all these setup, it is time to write mocks in your Storybook stories.

Mock data in Storybook

In our Storybook file, we first import the msw-utils which we created in the above steps. (Your import path might be different.) Then, we define the mock data, and export a story using the ProfileCard component (not included in this article).

// ProfileCard.stories.jsx...
import { graphql, mock } from "../../../msw-utils";
...const PERSON_MOCK = {
id: "478620",
name: { first: "David", last: "Cai" },
gender: "male",
age: 42
};
export const Profile = () => <ProfileCard />;

Now, here is the code to stub a response for a GraphQL query “Persons”:

// ProfileCard.stories.jsx...export const Profile = () => <ProfileCard />;
Profile.decorators = [
story => {
mock(
graphql.query(
"Persons",
(_req, res, ctx) => {
return res(ctx.data({
household: { persons: [PERSON_MOCK] } }));
}
)
);

return story();
}
];

ctx.data is used to return a mocked data as response. Persons is the query name we defined in the GraphQL document, for instance:

// Query name in a graphql documentexport const QUERY_PERSONS = gql`
query Persons {
household {
persons {
id
name {
first
last
}
gender
age
}
}
}
`;

For mocking mutations,

// ProfileCard.stories.jsx...Profile.decorators = [
story => {
mock(
graphql.query("Persons", ...),
graphql.mutation(
"UpdatePerson",
(req, res, ctx) => {
const { updatedPerson } = req.variables;
return res(ctx.data({
household: { persons: [updatedPerson] }
}));
}
)
);

return story();
}
];

To mock REST data, use rest instead of graphql, for example:

mock(
rest.get("/persons/:personId", (req, res, ctx) => {
const { personId } = req.params;
return res(ctx.json({ id: personId, firstName: "David" }));
})
)

Mock loading state

The ctx.delay function can be used to mimic network latency.

res(
ctx.delay(2000), // Delay response for 2 seconds
ctx.data({ your: data })
);

To keep staying in the loading state, you can replace the arbitrary milliseconds with “infinite”:

ctx.delay("infinite");

To mimic random latency, you can omit the argument:

ctx.delay(); // Random server response time

Mock error state

Use ctx.errors function to mock server-side errors (either GraphQL or REST errors):

res(
ctx.errors([
{ message: "Failed to find person" }
])
);

To mock network errors, use ctx.status:

res(ctx.status(404, "Resource not found"));

More utility functions can be found at https://mswjs.io/docs/api.

Integration with Jest

Component stories defined in Storybook are often shared with their unit tests, including the mocks. In order to share MSW mocks, we will need two things — the “@storybook/testing-react” plugin, and a Jest setup file.

The @storybook/testing-react plugin includes the Storybook decorators we created for each story. Since we define mocks using decorators, this plugin will automatically tie stories with their mocks. Run the following command to install it:

npm install @storybook/testing-react --save-dev

Once, this plugin is installed, we can import and compose stories with the composeStories utility:

// ProfileCard.test.jsximport { composeStories } from "@storybook/testing-react";
import * as stories from "./ProfileCard.stories";
const { Self } = composeStories(stories);describe("ProfileCard", () => {
it("should render a profile card", () => {
render(<Self />);
// ...
});
});

Before we run the test, we also need to setup Jest to start the Mock Service Worker, reset all mocks after each test, and close the worker once all tests are completed. To do that, we will need a Jest setup file like this:

// jest.setup.jsimport { server } from "./msw-utils";// Setup Mock Service Server for unit test
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

In project’s package.json, we register the setup file with Jest config:

// package.json"jest": {
// ...
"setupFilesAfterEnv": ["<rootDir>/jest.setup.js"]
}

Recap

Although it takes a few steps to set things up, most of these steps are one-time only.

  • Install msw and @storybook/testing-react (One-time setup)
  • Generate mockServiceWorker.js in a public directory (One-time setup)
  • Change storybook commands to include the generated mockServiceWorker.js (One-time setup)
  • Create msw-utils (One-time setup)
  • Start MSW in .storybook/preview.js (One-time setup)
  • Start MSW in Jest setup file (One-time setup)
  • Mock data in Storybook

Your project might have the following structure:

your-project
|
+ .storybook
| |
| + preview.js // Storybook preview setup
|
+ public
| |
| + mockServiceWorker.js // Generated script
|
+ src
| |
| + ProfileCard.jsx // Component
| |
| + ProfileCard.stories.jsx // Storybook stories
| |
| + ProfileCard.test.jsx. // Jest test
|
+ jest.setup.js // Jest setup
|
+ msw-utils.js // MSW utils
|
+ package.json

With this setup, we can mock HTTP responses at network level, without relying on any specific data-fetching library. We can also share MSW mocks between Storybook and Jest tests to maximize code re-usability.

Gamer, hiker, reader, and programmer