A Comprehensive Guide to Mock Service Worker (MSW)
Mock Service Worker (MSW) is a valuable API mocking tool that intercepts network requests, enabling seamless testing of components with network interactions. Adopting MSW simplifies testing setups, making it easy to create, share, and manage mock definitions for testing, development, and debugging. The article offers both basic and advanced practices for MSW, showcasing its effectiveness in handling complex projects with practical examples, including structuring handlers, testing errors, and optimizing the testing environment.
Mock Service Worker is an API mocking tool that lets you mock by intercepting requests on the network level. You can reuse the same mock definition for testing, development, and debugging.
MSW is delightful to adopt. Plus, it makes testing components with network requests a pleasure.
In this article, I’d like to share some best practices for working with MSW for basic and more advanced problems.
When to use a Mock Service Worker?
Has this ever happened to you?
You’ve finally gotten the green light to spend some writing tests!
This sprint is calm, but you remembered all too clearly that disastrous release a few weeks ago. The entire team had to drop everything and fix bugs.
Your repo has around 20% test coverage, and everyone agrees that it’s too little, but there’s never enough time to write tests. You write your features, get an LGTM on the PR, and move on.
But this release will be different!
You have a dedicated task, a couple of story points, and a jug of coffee to get this done right. You know the feature you’re about to test very well; you wrote most of it. You set out to mock the component and pass some props.
The tests fail.
No worries, that was only the first try, my friend! You re-check what needs testing, add some missing mocks, and maybe throw a <rte-code>jest.spyOn<rte-code> in there. You’re so proud of your accomplishment!
<rte-code>yarn test<rte-code> – another fail.
You dig deeper and realize your component depends on a network request returning some data.
Ok, don’t panic.
Let’s see how other tests in the codebase manage this. You find tests mocking <rte-code>axios<rte-code>, other ones' mock <rte-code>fetch<rte-code>, and wait; there are also some global files mocking resolved values and network errors?
You sweat a little, add many lines of code to your test, and ponder briefly if all of them are necessary. You notice tests unrelated to your changes started failing! You start feeling like the people in the first half of any infomercial.
…and you probably think: there has to be a better way! That’s when Mock Service Worker comes in handy.
The basic guide to Mock Service Worker (MSW)
Mock Service Worker intercepts real network requests that your tests make.
The components in your tests think they are talking with the real API, which means you can test their real functionality (as opposed to testing contained, mock functionality). If you don’t feel convinced about this approach just yet, I invite you to read Kent C. Dodd’s excellent blog post: Stop Mocking Fetch.
Let’s see this magical library in action. As with any new dependency, you need to start by adding it to your project: <rte-code>npm install msw --save-dev<rte-code> or <rte-code>yarn add msw --dev<rte-code>
Once the installation finishes, you will have to define your request handlers. MSW documentation suggests creating them in a file called <rte-code>mocks/handlers.js<rte-code>. Depending on your project, you can create REST or GraphQL handlers. MSW request handlers aim to hold instructions for specific API endpoints.
Let’s take a look at an example REST request handler:
We start by importing the <rte-code>rest<rte-code> function from MSW, then create an array of API endpoints we want to handle.
We can add anything we would like to this array. We can mock an error response; we can mock correct or faulty data objects. Remember to use absolute request URLs since we will use those request handlers in a NodeJS environment.
The only caveat here is that if you add multiple handlers for one API endpoint, the last one in the array will be the only one that’s actually used. But more on that later.
Once you have a few handlers ready, it is time to let your test suite know about them. We will need to set up a request mocking server and run it with our tests. This may sound scary, but don’t worry; the server setup file is three lines long.
Let’s create a new file called <rte-code>server.js<rte-code>:
We have to import the <rte-code>setupServer<rte-code> function from <rte-code>msw/node<rte-code> and pass the previously created request handlers to that function.
I don’t know about you, but this is the simplest mock server setup I have ever seen!
The very last step is running the mock server when you run your tests.
This step depends heavily on your project setup. There are various ways to set up and configure Jest, and this blog post will not try to cover all of them.
Instead, I’ll talk about the setup I’ve seen the most often: using a dedicated <rte-code>setupTests.js<rte-code> file, which is called through Jest’s setupFilesAfterEv.
If that’s the case in your project, congratulations! You only need to add the following lines:
If you prefer to test MSW quickly in a single test file, you can do so as well.
It is possible to use the mock server directly anywhere you would like. This means you can skip all the steps above (except for the installation, let’s not get carried away!) and write something like this in your test file:
This has all been fun and games so far, don’t you think?
Going through the setup takes less than an hour. Honestly, going through the official MSW documentation takes less than an hour!
If you’re working on a project that’s not too big, you can stop reading now, pat yourself on the back, and look at all those green check marks on your tests.
The advanced guide to Mock Service Worker
If you are working on a big application, maybe a robust dashboard, or an e-commerce website, you realize the basic setup presented above won’t cut it.
The handlers array will soon become a 10-thousand line mess. People may add mock servers in random tests, making debugging errors difficult.
My team and I have faced those issues, and here’s how we decided to tackle them.
As stated above, the biggest issue in a big app using MSW is the handlers' files becoming unreadable. We tackled this issue by dividing the handlers’ files into separate, feature-related files.
Now, instead of a single, long <rte-code>handlers.js<rte-code>, we had something in the shape of the following structure:
A few API endpoints which were commonly needed for the tests and did not clearly fit any feature were left in the <rte-code>commonMswHandlers.js<rte-code> file.
Once we’ve effectively sliced the <rte-code>handlers<rte-code> array, we started wondering how these specific request handlers should be consumed. As we saw above, we could let the test authors import <rte-code>setupServer<rte-code> with specific handlers directly in the test files.
However, this quickly became quite verbose. We decided to create helper functions in our <rte-code>mswHandlers.js<rte-code> files. An example file would look like this:
As you can see in the code above, we’re using the <rte-code>server.use()<rte-code> function. This function prepends request handlers to the current server instance. This means we are using everything that has been set up in the <rte-code>tools/jest/server.js<rte-code> file, and we’re adding a specific request handler for a specific URL.
Now, to use this helper function in a test, the test author would need to write something like this:
The test author must import the server helper and run it in a <rte-code>beforeEach<rte-code> block.
Tests in this particular test suite will now look into the <rte-code>homeHandlers<rte-code> array when the components need something from the *https://fancy-app.com/homepage-data* endpoint, while other test suites remain unaffected by this change.
What if you would like to test that your website can handle network errors on specific endpoints?
Let’s add another helper function to the <rte-code>FancyHomepageFeature/msw/mswHandlers.js<rte-code> file:
We can add tests for the scenarios described above: what should happen if the server returns a successful response, but the values are unexpected? How should the website behave if there is a network error?
Let’s see those tests in action:
Request handler overrides
If you read the MSW documentation carefully, you may be tempted to use the one-time override (<rte-code>res.once()<rte-code>) for testing errors.
We have tried using this strategy in our codebase. We have found that using <rte-code>res.once()<rte-code> is counter-intuitive to the test author. If the test setup includes this one-time override, but the test suite has more than one test, only the first test will behave as expected.
It may take the test author quite some time and great detective work to figure out that all his tests were correct, but the MSW handlers were prepared to handle only one request. If I was ever to use <rte-code>res.once()<rte-code>, it would only be directly in a test file, where it’s easily visible, and never in setup files.
Response object overrides
There is another type of override that I do recommend using - response object overrides.
Let’s say you would like to test your network request with different, successful responses. You could write separate functions using <rte-code>server.use()<rte-code>, but that’s a lot of repetitive work.
One of my colleagues, Jan Jaworski, came up with a very elegant solution to this problem. He created a helper function that could accept override data and use it in the response object thanks to the spread operator.
The function looked roughly like this:
Here’s how this helper function would be used in a test:
This strategy is especially useful for big data objects we don’t want to re-type multiple times.
Testing errors - bonus content
Since we started testing network errors, we discovered the terminal output got polluted with many <rte-code>console.error<rte-code> messages. These error messages were expected, and we didn’t want them hiding real (as in UNexpected) errors.
We decided to add a <rte-code>console.error<rte-code> override in the Jest setup file:
Thanks to this little function, we hid only the specific console errors we were sure about.
If you were to take away only one thing from this blog post, I sincerely hope it would be the notion that MSW is a great tool worth trying, even in a big project.
The solutions I described worked great in a real project. They may not be 100% ideal for your needs, but I hope you find some inspiration here to create the best, most comfortable, and developer-friendly test setup possible.