Skip to content

Instantly share code, notes, and snippets.

@imdongchen
Created April 29, 2021 15:41
Show Gist options
  • Save imdongchen/07d7b06252a846273de505553b7c8dc5 to your computer and use it in GitHub Desktop.
Save imdongchen/07d7b06252a846273de505553b7c8dc5 to your computer and use it in GitHub Desktop.

Problem

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. I will also share my learning in debugging why mocking XMLHttpRequest doesn’t work.

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 it without third-party library). We created a React component wrapper

https://gist.github.com/b4fa8319b6e7516f9bc0da533a7466a8

Then we can use it in storybook https://gist.github.com/8ec4c05028303d69198ebc7f62689123

And a cool thing is that I can import the story in my test https://gist.github.com/ada0143e581a2a1c4cd4634ff04c9d52

Note that we are defining server mock response and component props only once and reuse them in test. Essentially we are creating stories for components in different scenarios (visual testing) and programmatically unit test each story. Really cool, isn’t it?

…Until the test actually breaks https://gist.github.com/15507a9df9b4903f35af9db99aef1070

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.

https://gist.github.com/0eeba00e29802bede998e5eed4a5f1c6

Not only superagent but other API clients (e.g. axios) do the same thing. The reason is that XMLHttpRequest is a browser object (accessed with 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:

https://gist.github.com/d8ad64538428a1d398966e2e7154de51

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 you can decide how to handle each request. This solution is better because

  • Real http requests are happening. That means you can see http requests in the browser network tab. This is in contrast with mocked API clients. No network calls are made and this could be confusing and lead developers to think the component did not make server requests.
  • It’s closer to really user experience. No http client is mocked and they 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. https://gist.github.com/ce0cc566ca1527880a7c0e8dba10a6c4

One challenge is that msw uses slightly different API for browser and nodejs. Let’s create a React wrapper to create a consistent API: We can use it the same way as before

https://gist.github.com/cb3901a8f9ea865c0caf17459d82d446

We need to setup the msw server (for unit test) and service worker (for browser) https://gist.github.com/ab6680508d647e0b2a03bda30fa46748

And a bit more setup for msw:

In unit test setup (setupFilesAfterEnv for jest) https://gist.github.com/b83612b239520d4530a108aa5adcd1f8

In .storybook/preview.js, start service worker https://gist.github.com/3ff86176a9de1893bba2fd6f126cb221

Finally create a service worker script with msw cli:

https://gist.github.com/c8e55060169f471fbaa84c0b98fedf32

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: https://gist.github.com/9c314bdac8f66ec27753adfa37c14a30

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment