DEV Community

Mike Schutte
Mike Schutte

Posted on

Integration stories: elevating Storybook with Mock Service Worker

During our last product cycle at Process Street, our development experience was heavily aided by combining Mock Service Worker (MSW) and Storybook. It's a powerful union of tools worth knowing about.

Storybook with MSW

Building a rich text editor

At Process Street, we're adding rich text documents ("Pages") to our app to help people document their processes faster without having to learn right away about our more structured workflow models.

We chose Slate.js to be the engine of our text editing features, topped off with UI composed by Chakra components.

You don't need a re-write to improve developer experience

Process Street is one of many teams who has done its darnedest to keep up with the rapidly evolving JavaScript landscape whilst iterating rapidly on product. The product started as an AngularJS (1.5.6) application and has gradually adopted more and more React components using react2angular.

The surface area of our product is already huge, and the frankenstein-state of our UI architecture adds some...friction...to the development experience in certain ways. Instead of loading up the entire application and depending on a locally running API server, we do all of our new UI work in React starting with Storybook.

Getting integrated

In my 3+ years of using Storybook to build user interfaces, most of the story-able components end up being leaves or very close to leaves in the DOM tree: the ones that are basically taking props and rendering JSX. This is a great pattern for rendering myriad states as named stories, but it's always been a challenge to move up the tree and achieve what is essentially an integration test, but in the form of an interactive story.

Modern React features and patterns make the demand for integration stories even greater. With the rising popularity and leverage of React's hook and context APIs along with adopting more colocated network fetching patterns (e.g., react-query and Apollo Client) in place of a global store like Redux, it's becoming more common and instinctual to build opaque components (meaning you don't pass them props) that fetch their own data via hooks and context providers.

The same patterns and tools that have allowed us to write more powerful, loosely coupled, and extensible components have left us in somewhat of a pickle when trying to express these components as stories without going through a kind of surgery to separate prop-driven UI components from wrapping components that take care of sourcing those props.

Enter Mock Service Worker.

Mock by intercepting requests on the network level. Seamlessly reuse the same mock definition for testing, development, and debugging.

By combining MSW with Storybook, you can express components anywhere in the DOM tree as interactive stories. Here's how I got an integration story set up for that Pages feature.

Implement a working MVP in the app

Similar to testing, you can either build your story after you have a working feature, or use the story to drive the component's development. I like starting with a low-fidelity working feature in the app, then moving to Storybook to refine the UI.

All of the necessary API endpoints for this feature already existed, so I opted to build the MSW handlers based on real data from the server. To do so, I observed the network traffic in the developer console while exercising the feature. After that I copied (via right-click) the relevant requests and responses related to the feature's interaction points.

copy fetch

copy response

Wire up MSW for Storybook

I'm going to focus specifically on how we integrated Storybook with MSW, ignoring setup. Check out this article for a more thorough walk through of setting everything up. While we're not using it yet in our app, I recently learned there is even a Storybook addon that simplifies the integration. For educational purposes, I'll still walk through the DIY setup we currently use.

Given a standard MSW setup, first make sure the service worker is kicked off in preview.js.

// .storybook/preview.js

import { worker } from '../src/mocks/browser';

// this allows you to simply use `worker.use` in your story and/or story decorators
worker.start();
Enter fullscreen mode Exit fullscreen mode

In my case there was a lot of API responses that would clutter the story itself, so I exported an object with named keys from a file called story-data.

// story-data.ts
export const data = {
  latest: { ... }
}
Enter fullscreen mode Exit fullscreen mode

With the response data in place, import data and configure the worker.use API from MSW using the Storybook template pattern.

import { data } from './story-data'
const Template: Story<Args> = (props) => {
  worker.use(
    rest.get("/latest", (req, res, ctx) => {
      return res(ctx.json(data.latest))
    },
    ...
  )
  return <Component {...props} />
}
export const Demo = Template.bind({})
Enter fullscreen mode Exit fullscreen mode

I prefer to put worker.use in the story component itself instead of a decorator because I often end up using Storybook args to influence the MSW response data.

const Template: Story<Args> = ({ loading, ...props }) => {
  worker.use(
    rest.get("/latest", (req, res, ctx) => {
      if (loading) {
        return res(
          ctx.delay(1000 * 60),
          ctx.json(data.latest)
        )
      }
      return res(ctx.json(data.latest))
    },
    ...
  )
  return <Component {...props} />
}
Enter fullscreen mode Exit fullscreen mode

Another thing to remember is you can often leverage the request object data (req) to influence the response data.

const Template: Story<Args> = ({ loading, ...props }) => {
  worker.use(
    rest.get("/latest/:id", (req, res, ctx) => {
      if (loading) {
        return res(
          ctx.delay(1000 * 60),
          ctx.json(data.latest)
        )
      }
      return res(ctx.json(data.someLookup[req.params.id]))
    },
    ...
  )
  return <Component {...props} />
}
Enter fullscreen mode Exit fullscreen mode

From here, you can get creative combining Storybook arguments and request data to simulate full interaction with a remote data source. Now in addition to observing with and interacting with specific UI states, we get to leverage MSW and Storybook to represent specific server and network states. Similar to the ethos of integration tests using React Testing Library, we're testing and exercising our components in a way that is much closer to how a user will interact with them in production, which is a good, good thing.

If you're interested in working at Process Street, we're hiring!

Top comments (0)