- Summary
- Mock Data
- REST
- Mocking Success
- Mocking Error
- Grouping Routes Together
- Mocking with mixed Success/Error
- Consuming MSW Routes in a Storybook Story
- GraphQL (Coming Soon)
Summary
Using MSW + MSW Storybook Addon, we are able to proxy any API calls to a Service Worker that's located in the ./public
folder. Below we will go into the steps suggested for setting up a story that requires data from multiple different types of APIs and how they're each mocked with MSW.
We will take a heavier focus on REST APIs, simply because these are most likely what a majority of developers would be asked to mock - but if this does not interest you, feel free to check out the GraphQL guide at the bottom!
Mock Data
In an ideal world, the teams responsible for the endpoints you are consuming will have easily accessible data contracts / mock data. If this is not the case, try to identify successful responses from each of your endpoints to curate your own. Storing mocked data separate from your routes makes the data integrity easier to maintain over time and at scale. It's significantly easier to import the data to your mocked routes than it is to scroll through a seemingly infinite sea of JSON.
// data.js
export const FilmOneMockData = {
// data pertaining to Film One here
}
export const FilmTwoMockData = {
// data pertaining to Film Two here
}
// script.js
import { FilmOneMockData, FilmTwoMockData } from "./data"
// consume mocked data here
REST
Let's compare an Express route to an MSW route to highlight similarities between what might already be familiar to most developers. Keeping things as simple as possible, we will start with a route that returns a payload { message: "hello world" }
when a GET request is sent to the route "/helloWorld"
.
// Express GET Route
const express = require('express')
const app = express()
app.get('/helloWorld', function (req, res) {
res.json({ message: 'hello world' })
})
// MSW GET Route
import { rest } from "msw";
rest.get('/helloWorld', (req, res, ctx) => {
return res(ctx.json({ message: 'hello world' }))
})
Both tools call a similarly composed function to api.get(route, handler)
, where the first argument passed is the route associated with the GET request and the second argument is the handler function where we can determine the logic associated with this route.
MSW's context ctx
is an argument of a response resolver that contains a set of response transformers to compose a mocked response. You can find these transformers listed in the side bar under context here, where we link you to the most commonly used ctx.json()
Mocking Success
// MSW GET Route - Success
import { rest } from "msw";
rest.get('/helloWorld', (req, res, ctx) => {
return res(ctx.json({ message: 'hello world' }))
})
Mocking Error
You can change the HTTP error code passed to ctx.status()
to mock particular error scenarios, as well as modify the delay of response (in milliseconds) by modifying the value passed to ctx.delay()
.
// MSW GET Route - Error
import { rest } from "msw";
rest.get('/helloWorld', (req, res, ctx) => {
return res(ctx.json(ctx.delay(800), ctx.status(403)))
})
Grouping Routes Together
Below, we cover how to compose arrays of routes so that we can bundle them together for consumption. The largest benefit to this is the ability to recreate specific scenarios within your application/component where the endpoints it consumes may be in varying states of up or down.
import { rest } from "msw";
import { AuthMockData, CheckoutMockData } from "./data"
const MockedRoutes = [
rest.get("/api/auth", (req, res, ctx) => {
return res(ctx.json(AuthMockData));
}),
rest.get("/api/checkout", (req, res, ctx) => {
return res(ctx.json(CheckoutMockData));
})
]
Mocking with mixed Success/Error
This will enable us to mock cases where only SOME services/endpoints are down so that we can determine how to build our frontends with high quality error handling
Let's use an ecommerce store as an example, where our two consumed routes are /api/auth
and /api/checkout
. Users of this store will not be able to check out with their account associated details if the /api/auth
route is failing and may have to checkout as a guest, if supported by the store. It would also not be possible for the users of this store to checkout if the /api/checkout
route is failing.
Possible route success/failure states
/api/auth
✅ & /api/checkout
✅/api/auth
✅ & /api/checkout
❌/api/auth
❌ & /api/checkout
✅/api/auth
❌ & /api/checkout
❌Consuming MSW Routes in a Storybook Story
Using the scenarios from the previous example, let's take a closer look at what consumption of our newly mocked mixed Success/Error routes would look like.
// ./routes.js
import { rest } from "msw";
import { AuthMockData, CheckoutMockData } from "./data";
const MockedSuccessRoutes = [
rest.get("/api/auth", (req, res, ctx) => {
return res(ctx.json(AuthMockData));
}),
rest.get("/api/checkout", (req, res, ctx) => {
return res(ctx.json(CheckoutMockData));
})
];
const MockedErrorRoutes = [
rest.get("/api/auth", (req, res, ctx) => {
return res(ctx.delay(800), ctx.status(403)));
}),
rest.get("/api/checkout", (req, res, ctx) => {
return res(ctx.delay(800), ctx.status(403)));
})
];
const MockedOnlyAuthUpRoutes = [
rest.get("/api/auth", (req, res, ctx) => {
return res(ctx.json(AuthMockData));
}),
rest.get("/api/checkout", (req, res, ctx) => {
return res(ctx.delay(800), ctx.status(403)));
})
];
const MockedOnlyCheckoutUpRoutes = [
rest.get("/api/auth", (req, res, ctx) => {
return res(ctx.delay(800), ctx.status(403)));
}),
rest.get("/api/checkout", (req, res, ctx) => {
return res(ctx.json(CheckoutMockData));
})
];
// ./App.stories.js
import React from "react";
import { App } from "./App";
import {
MockedSuccessRoutes,
MockedErrorRoutes,
MockedOnlyAuthRoutes,
MockedOnlyCheckoutRoutes
} from "./routes";
export default {
title: `App w/ Mocked Routes`,
component: App,
};
export const DefaultBehavior = () => <App />;
const MockTemplate = () => <App />;
export const MockedSuccess = MockTemplate.bind({});
MockedSuccess.parameters = {
msw: MockedSuccessRoutes,
};
export const MockedError = MockTemplate.bind({});
MockedError.parameters = {
msw: MockedErrorRoutes,
};
export const MockedOnlyAuth = MockTemplate.bind({});
MockedOnlyAuth .parameters = {
msw: MockedOnlyAuthRoutes,
};
export const MockedOnlyCheckout = MockTemplate.bind({});
MockedOnlyCheckout.parameters = {
msw: MockedOnlyCheckoutRoutes,
};