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.
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!
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.
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!
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!