Effortless API Mocking and Testing with Mock Service Worker
TL;DR
In this article we will discover another way to mock your backend APIs using a library named Mock Service Worker.
We will mainly cover:
- What is MSW
- MSW solution vs conventional solutions (why MSW).
- Intercepting APIs request (get, post… etc.).
- Node environment
- Using MSW for integration tests
- When NOT to use MSW.
What is Mock Service Worker
MSW: is an API mocking library that is built on top of the Service Worker API to intercept outgoing requests.
It supports both REST and GraphQL calls, and it works on both the client side (browser) and on the server (Node environment).
Why use tools like Mock Service Worker
When you want to mock data provided by an API, there are two traditional ways to do it:
- Using hard coded data
- Provide a dedicated mock server as a substitution
Hard coded data
This approach relies on using static data to mock APIs results.
It’s easy to implement, but it has a many drawbacks:
- Static data clutters your code.
- Some degree of rework is required once the actual APIs are ready, which adds development overhead.
- Non interactive/static: hard coded data means no dynamic interaction with the server (e.g. Response based on user input, different status codes).
Dedicated mock server
This approach involves setting up a dedicated mock server as a fallback for consuming the different APIs.
The main advantage of this approach is also its biggest drawback, the fact that we are setting up a server at the end of the day:
- Setting up a server requires some degree of maintenance, thus extra development effort.
- Spending time working on the mock server takes away from the development hours.
- Transition from the mock server to the real server requires some adjustments to the application’s code.
So… what’s the solution then??!
Before we delve into the solution, let's clarify a few things first: what does mocking means? and what differentiate good mocks from the not so good ones?
Mocking means creating a fake version of an external or internal service that can stand in for the real one - CircleCi blog
So knowing what mocking is, let’s see what are the criteria to judge whether a mock is good or bad.
- Ideally, when mocking a behavior or an object, we aim for the mock to closely resemble the real thing, which means it should mimic the expected behavior in various scenarios.
- Another requirement is a smooth and seamless transition from the mock to the actual API, with minimal rework.
These two criteria can determine if our mocking strategy is good or not.
Mock Service Worker
- Mock Service Worker intercepts requests at the network level, which is the closest thing to a an actual server (1st criteria) and is different from the requests interception on the application level (hard coded data).
- The whole logic of the Mock Service Worker, can be switched ON/OFF thanks to the functions
start
(browser)/listen
(Node), this means we can control its scope by wrapping it in anif
statement. (2nd criteria).
A bonus point is that mock definitions are reusable which allows us to use the same code not only for testing, but for development and debugging as well.
Intercepting Requests
Browser environment
Mock Service Worker uses Service Worker API, which means it can handle the fetch request as they leave the browser, giving the illusion that the request has been sent and that the response has been received.
The image below illustrates how Mock Service Worker handles fetch requests and returns mocked responses:
Source: https://mswjs.io/docs/
Implementation
The MSW API provides all the tools to construct a controller-like object that handles your request and response.
For the browser environment, an object called setupWorker
is responsible for registering the request handlers.
Request handler is basically a function that takes a request URL and a callback to handle that request (request resolver). It determines whether an outgoing request should be mocked, and specifies its mocked response. It has all the HTTP methods : (GET, POST, PATCH …etc.)
Response resolver is a function that accepts a captured request and may return a mocked response. It takes as parameters:
request
: Information about the captured request.response
: Function to create the mocked response.context
: Context utilities specific to the current request handler.
A typical setupWorker
will look like this:
import { setupWorker, rest } from "msw";
const worker = setupWorker(
// Provide request handlers
rest.get("/user/:userId", responseResolver1),
rest.post("/users", responseResolver2),
rest.delete("/users/*", responseResolver3)
);
// Start the Mock Service Worker
worker.start();
The worker object can be started/stopped conditionally with the functions worker.start()
or worker.stop()
respectively.
Example for manipulating request/response elements:
- setting a delay for a specific endpoint:
import { setupWorker, rest } from "msw";
const worker = setupWorker(
rest.get("/user/:userId", (req, res, ctx) => {
return res(
// Delays response for 3seconds.
ctx.delay(3000),
ctx.json({
data: "mocked Data",
})
);
})
);
worker.start();
- conditional response:
import { setupWorker, rest } from "msw";
const worker = setupWorker(
rest.get("/user/:userId", (req, res, ctx) => {
if (userId === "real-user Id") {
// let the request pass to the real server
return req.passthrough();
}
// Otherwise, always respond with a mocked response.
return res(
ctx.json({
id: "abc-123",
})
);
})
);
worker.start();
- set specific status code:
import { setupWorker, rest } from "msw";
const worker = setupWorker(
rest.get("/user/:userId", (req, res, ctx) => {
res(ctx.status(404)); // returns a specific code
})
);
worker.start();
Node environment
In the browser, request interception is done thanks to the service worker API, but in a non-browser environment (node server) it's done by extending native http
, https
and XMLHttpRequest
modules (see: https://github.com/mswjs/interceptors)
Implementation
The difference in implementation is only apparent in the object responsible of registering the request handlers
import { setupServer, rest } from "msw";
const server = setupServer(
// Provide request handlers
rest.get("absolute-URL", responseResolver)
);
// Start the Mock Service Worker
server.listen();
So all the other logic (request handlers, request resolver) can be shared between browser and Node environments.
⚠️In Node environments, all registered URLs must be absolute.
Integration testing
Although Mock Service Worker can be used during development, it is most often used for intercepting APIs during integration testing.
Integration testing is a type of testing where software modules are integrated logically and tested as a group. - ISTQB
Since tests don’t run in the browser but rather in the server, we will only be using the server instance setupServer
.
The typical test setup using jest should look like this:
//server is the instance of setupServer we create: (const server = setupServer(...handlers))
if (typeof window === "undefined") {
beforeAll(() => {
server.listen();
});
//clean all handlers after each test
afterEach(() => {
server.resetHandlers();
});
//close the msw server
afterAll(() => {
server.close();
});
}
This is all the configuration that should be done for the server instance to be setup correctly.
The Request Handlers should take care of the specific absolute URLs that need to be mocked.
When Not to use the MSW
MSW is meant to be a replacement for your backend servers, so its use should be restricted to the development environment only.
💡 It is highly recommended to wrap worker.start and server.listen in a development condition
Here’s a few examples of when NOT to use MSW:
- In development, if your endpoints are relatively easy to implement, just use real server instead.
- Using MSW as a real server in production.
Recap
As we can see, using MSW allows us to have full control over:
- The request and response body
- Status and the headers
- Response delay
On top of that, we also have the benefit of:
- The seamless transition between the mocked state and the real one, either by switching On/Off the entire object using
start
/listen
functions, or by configuring the request handlers individually usingif
statement inside the response resolvers. - The flexibility to partially mock by capturing specific paths or patterns while allowing other endpoints to pass through
This flexibility comes in handy in software developed in an agile, competitive environment where there are shifting deadlines, parallel implementations and continuous delivery.