API mocking in unit test and storybook

Dong Chen
ITNEXT
Published in
4 min readApr 29, 2021

--

Photo by Christina @ wocintechchat.com on Unsplash

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!

--

--