Mock GraphQL and REST in Storybook and Jest with MSW

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.

MSW mocks for GraphQL queries and mutations in Storybook
Mocked requests show up in browser’s dev tools

Install

First, install MSW:

npm install msw --save-dev
npx msw init <PUBLIC_DIR> --save
// package.json"msw": {
"workerDirectory": "public"
}

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"
}
// 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";
// 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";
// .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"
});
  • 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.
your-project
|
+ .storybook
| |
| + preview.js
|
+ public
| |
| + mockServiceWorker.js // Generated script
|
+ src
|
+ msw-utils.js // or msw-utils.ts
|
+ package.json

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 />;
// 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();
}
];
// Query name in a graphql documentexport const QUERY_PERSONS = gql`
query Persons {
household {
persons {
id
name {
first
last
}
gender
age
}
}
}
`;
// 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();
}
];
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 })
);
ctx.delay("infinite");
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" }
])
);
res(ctx.status(404, "Resource not found"));

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.

npm install @storybook/testing-react --save-dev
// 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 />);
// ...
});
});
// jest.setup.jsimport { server } from "./msw-utils";// Setup Mock Service Server for unit test
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// 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
|
+ .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

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
David Cai

David Cai

Gamer, hiker, reader, and programmer