Storybook and Mock Service Worker, a match made in heaven
Storybook is an awesome tool for UI development. And as you’ll know if you read my blog post about integration testing, Mock Service Worker is also a very powerful tool for UI testing. When you put them together, you can do some really cool things:
- Get a head start on UI development when the API endpoints you need aren’t available yet.
- Visual regression testing on complex pages and components (through Chromatic or Storyshot).
- Demonstrate edge case states in the UI without having to set up users with real data.
- Make it easier to demo applications and changes to folks in design & product.
At HMH, we achieve all this by reusing our mocked data endpoints from our integration tests with Mock Service Worker, with some tweaks to our microfrontend apps to get them appearing correctly in Storybook.
Let’s hook up MSW to Storybook
No point reinventing the wheel! We got ourselves started by following a blog post to connect MSW and Storybook.
We also followed the examples Mock Service Worker provide specifically for Storybook.
Now, that’s not all we had to do. There was a bunch of extra stuff to get our specific configuration over the line as I’ll describe later, but eventually we were able to produce a variant of our teacher assignments details screen, complete with Harry Potter-themed mock data and almost fully interact-able elements, as if the page was running on our Ed product in real life.
Up until this point we might have shown different components of that page in Storybook individually (like the sortable table for the students, or the button to show/hide the deletion modal). In terms of our microfrontend architecture, this Storybook entry is now rendering a full app — complete with its own React Router and hierarchical tree of various components, and a whole host of data-querying capabilities.
Before this, we never had the power to truly showcase our apps with specific data without having to set it up in real life in one of our environments. This approach saves us a lot of time — our most valuable resource as engineers! It also opens up the possibility of more automation for visual testing, less UI bugs, and therefore, higher quality code and happier customers!
Some gotchas after going through the process
HTTPS is required for service workers
In case you didn’t know already, HTTPS is necessary to be able to use service workers. If your deployed version of Storybook is not on a secured protocol (as it happens, our “lowest” environment version of Storybook was not!), you won’t be able to use Mock Service Worker. You can test it on http individually with a browser-config setting but that’s not feasible by any means as a long term solution. Your best bet is to use HTTPS.
Make your mock handlers reusable with relative paths
For REST request endpoints mocked in MSW handlers, you can use wildcard
characters in the URL to make sure the handlers work both for your integration tests, and in your deployed Storybook instance. This will turn your URLs into relative paths so that the API calls work across environments, e.g. rest.get('*/api/classes'...
instead of something like rest.get('https://localhost.com/api/classes'...
. MSW don’t really recommend wildcards in general, but if you’re only using these handlers in tests and Storybook, it’s low risk.
GraphQL handlers are a different story, as they rely on the query name (e.g. graphql.query('getClasses'...
)— so they’re already environment-agnostic.
Use MemoryRouter when mounting apps in Storybook
You might remember in my integration testing blog post that we encourage the use of real BrowserRouters and window
object manipulation in our integration tests, so that we can test when URLs are updated correctly in our jest tests. But you can’t do that in Storybook now that you’re running in a real browser! You’ll need to export versions of your apps without any React Router, so that you can wrap them in MemoryRouter
in your app.stories
file.
This also means you can’t do flows in Storybook that update the URL (like how the “View Report” button on our assignments screen above would normally bring you to the reporting app). This is ok though just to showcase pages in general — you don’t want to rebuild your whole product in Storybook!
Stay organised with an “app-stories” directory pattern
Our microfrontend apps are fairly well structured — apps live in packages/apps
in our monorepo and each of those apps might have folders like /src, /integration-tests, /handlers
, etc. We made a specific file pattern for /app-stories
, to differentiate them from other stories and to make sure our engineers keep their stories close to the apps they actually relate to (which is easier to maintain!). In our main.js
Storybook config file, we included this pattern:
stories: [
'../../../../packages/apps/**/app-stories/*.stories.@(js|jsx|ts|tsx)',
// Our other patterns
],
Interesting sidenote, I read an article recently about how the current generation of college students are struggling with the concept of file folders and directories. It’s absolutely wild, and so fascinating — we’ll have to just wait and see if and how the software world adapts.
You will need to include the generated service worker in your repo
One of the steps when you follow the guides above is to generate a mock service worker file, and hook it up to your Storybook startup configuration. It wasn’t immediately obvious to me, and the generated file will have a warning about not using it on production in the file comments, but you will need to check the generated MSW file into your repository and deploy it alongside Storybook. Otherwise your Storybook won’t have a service worker to rely on!
As a nice touch, MSW will also give you a warning in the console if you're using a generated mock service worker file spun up from an older version of MSW. This is actually really handy as it will encourage us to keep our testing packages up to date. (Of course, if you’re using something like renovatebot, you won’t have that problem!)
If your Storybook isn’t private, be cautious and hide that product!
Depending on your Storybook configuration and the decisions of your team, you might want to include some functionality to hide these page-level stories — no use giving away the whole farm to your competitors!
When we were implementing this as a proof-of-concept to showcase to the wider technology group, we included some functionality to only show app stories in development mode (i.e. when running Storybook locally). So that same story I had above would look something like this in our real production Storybook:
We managed this by writing a custom Storybook decorator that checks what environment you’re running in before mounting a story, and wrap all our app-level stories in this decorator.
Handling environment-specific logic (e.g. PROD vs lower environment API URLs) in Storybook
We deploy an instance of Storybook to each of our environments as part of the build process at HMH. Due to legacy reasons (a nice phrase that really means “I don’t know exactly why but it works!”), we set some environment stuff in window
tags on our HTML files from our Docker build during deployment of our Ed product. To get our apps working on Storybook, they also need access to these variables (in order to be able to determine which endpoints to call per environment).
The hack we used was to follow Storybook’s guide for adding scripts to the HTML head of the preview iframe (where your story will be rendered). We had to do a bunch of test deployments (and write a how-to doc on doing test deployments while we were at it) to make sure those attributes were being set in higher environments and to enable us to use MSW going forward.
Mock authentication in Storybook
Mocking authentication was a nice tricky problem, and easily where we spent the most time in this process. It’s almost impossible to mock anything in session storage from your stories in Storybook (because Storybook uses iframes and trying to access browser session storage from an iframe violates same-origin security policies). So if you rely on session storage for the results of your authentication (i.e. the tokens and all that fun stuff that get after login), you’ll need to mock the imports for your authentication UI code.
Firstly, we had to isolate and streamline the import of our authentication helper in the UI code so that we could mock the import altogether. So anything that was getting import { getUserCtx} from 'authentication-helper'
needed to be more precise, and be changed to import { getUserCtx } from 'authentication-helper/src/userContext/auth'
. That way, when we mock authentication-helper
, we won’t be interfering accidentally with any other helper code in there that our UI code might be relying on.
In our Storybook webpack.config
, we added an alias for any imports from that path:
config.resolve.alias[
'authentication-helper/src/userContext/auth'
] = require.resolve('../__mocks__/auth.js');
As you might have guessed, we stick a new file in the __mocks__
folder relative to our Storybook code, with whatever specific methods we need to be able to mock in our stories and an exported userContextDecorator
method. We need to be able to specify the userId, the role, and all sorts of things for our apps to run, but it might be different for you — I’d recommend having a look at the isomorphic-fetch.js
mock in the Storybook documentation at this point.
Stringing it all together, this is what our Storybook preview.js
configuration looks like:
What’s next?
We’re still super early in our journey with Storybook and Mock Service Worker, but I’m delighted to say that since implementing this pattern, one of our UI teams has already been able to do the first point on our list and get a head start on their next feature without waiting for their APIs to be ready!
It’s also become a very useful tool for onboarding folks to different aspects of the product — it’s much easier now to show people the different variations of an app and features that we’ve worked so hard to build.
As for what we’ve got our eyes on next, Storybook recently showcased an awesome pattern for using Storybook entries in unit tests. If we could reuse the same app entries we have in Storybook for our integration testing, we could reduce code duplication and boilerplate even more, make our engineers happier and code reviews easier, and increase confidence in our tests.
In the meantime, we’re working on getting more of our apps on Storybook and getting this approach available for team members outside development. Like the tortoise and the hare, slow and steady wins the race!
Since drafting this article, I have moved on from HMH to a new role. My HMH colleagues and friends have very kindly allowed me to have a couple last hurrahs for the engineering blog, and I can’t thank them all enough for the support, and the great times we had both in and out of the office! Thanks for reading, and stay safe!