This post explores three levels of API mocking and how we can make mocking work the same way for both storybook and unit test. I will also share my learning in debugging why mocking XMLHttpRequest
doesn't work. If you want to go ahead and see the best solution, check Option Three: msw
.
Why
In Web development, we don’t want to rely on backend API for several reasons:
- the backend is not ready;
- speed up UI development cadence
Typically we would mock API response for development in storybook and unit test. Solutions exists for storybook ( example) and unit test ( example) separately. My goal is to have a consistent API mocking for both storybook and unit test and reuse storybook stories in test.
Option One: mocking XMLHttpRequest
My project uses superagent
as the API client, which uses XMLHttpRequest
under the hood. I started with mocking out XMLHttpRequest
with xhr-mock (or you can mock without third-party library). I created a React component wrapper
We can use it in storybook
A cool thing is that we can import the story in our test
Note that we are defining server mock responses only once and reuse them in the test. Essentially we are creating stories for components in various scenarios (as visual testing) and programmatically unit test each story. Really cool, isn’t it?
…Until the test actually breaks
The error shows our unit test still tries to make a real http request even with our mock. What’s going on?
It turns out superagent
uses XMLHttpRequest
under the hood in the browser and node:http
in nodejs environment. In superagent
package.json you can see different files are loaded in browser vs. nodejs.
Not only superagent
but other API clients (e.g. axios
) do the same thing. The reason is that XMLHttpRequest
is a browser object (accessed via window.XMLHttpRequest
). It's not an object in nodejs; instead, nodejs uses node:http
for API request.
Now it should make sense why our test fails with XMLHttpRequest
mocking: Unit test is run in the nodejs environment and superagent
is not invoking XMLHttpRequest
at all!
Option Two: mocking superagent
The second option is to mock on a higher level: our API client ( superagent
in my case, same for axios
or others). Similar to xhr-mock
, people have made libraries for mocking superagent
(e.g. superagent-mock). Let's replace our React wrapper:
Similarly, we can use this wrapper in our component story to mock the server response and reuse it in unit test. This should work for both 🎉
But it can be better.
Option Three: MSW
msw provides the highest possible level mock without creating a server. The idea is use service worker to intercept all requests and we can decide how to handle each request. This solution is better because
- Real http requests are happening. That means you can see network calls in the browser
network
tab. This is in contrast with mocking API clients: No network calls are made and this could be confusing and mislead developers to believe the component itself did not make any requests. - It’s closer to real user experience. API clients are working the same way as in production. It brings more confidence that things are really working.
- You can do similar server checking when handling the requests, e.g.
One challenge is that msw
uses slightly different API for browser and nodejs. Let's create a React wrapper to provide a consistent API:
msw uses service worker for browser and monkey patch http:node
for nodejs. The React wrapper uses the proper msw integration to return the mocked response regardless of the running environment. And now you can reuse your mocking in both storybook and unit test just like what was described in Option One.
And a bit more setup for msw
:
In unit test setup ( setupFilesAfterEnv
for jest)
In .storybook/preview.js
, start service worker
Finally create a service worker script with msw cli
$ npx msw init public
This will generate a script (service worker interception implementation) in the public
folder. We should include this file in Git and start storybook with it:
$ start-storybook -s public
Now you are good to go!
Summary
This post describes three options to mock API, from low level to high level. I suggest high level mocking to get the closest production experience. Plus, reuse code between storybook and unit test is a real boost in developer experience!
Please give your loudest👏 (s) if you find this post helpful and follow me on twitter!