Introduction:
Very often, the frontend team starts their work, and the backend APIs are still not ready. Instead of waiting for APIs to be completed, frontend developer can use Mock Service Worker (MSW) to create mock Request Handlers. MSW will intercept network requests of the application. On finding a matching Request Handler, it will provide a mock response as configured by the developer. Mock responses can be configured in detail to mimic an actual API response very closely. Once the backend APIs are completed, developers can replace the mock API URL with the actual one.
We can use mock APIs for testing too. Since the testing is not dependent on actual data, we can test edge cases and increase the code coverage. If developer is dependent on actual data, they may not be able to test all possibilities.
In most cases, we can set up the Service Worker and Request Handlers within the application. If the application is huge with many end points, MSW also provides a feature to spawn a HTTP server. This server will then handle the requests and provide mock response. We can start the HTTP server whenever we want application to run using mock APIs.
In this blog, we will cover the basics of using MSW in above scenarios.
- Using MSW within Application
- Using MSW for Testing
- Using MSW from an HTTP Server
1. Using MSW within Application
1.1 Initializing MSW
First, we need to install MSW and add the Service Worker. Following code adds the Service Worker file (mockServiceWorker.js) within the public folder.
// Code snippet 1 - terminal
npm i msw –-save-dev
npx msw init ./public
1.2 Adding Request Handlers
Starting from version 2.0 onwards, we need to import the http object from msw package and add request handlers to it. For earlier versions, we would import the rest object. We can create an array of such objects and each array element will handle one request. Overall, the handlers will appear as below.
// Code snippet 2 – src/mocks/handlers.js
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users/list', () => {
......
}),
http.get('/api/users/:id', ({params}) => {
......
})
]
We will use the following data to demonstrate few request handlers in detail.
// Code snippet 3 – src/mocks/handlers.js
users: [
{ id:1, email: 'anil_t@email.com', firstName: 'Anil’, lastName: ‘T’,
roles: ['USER', 'HOUSE ADMIN'] },
{ id:1, email: 'george_p@em.com', firstName: 'George’, lastName: ‘P’,
roles: ['ADMIN'] },
{ id:1, email: 'sera_m@email.com', firstName: 'Sera’, lastName: ‘M’,
roles: ['USER'] },
{ id:1, email: 'raji_k@email.com', firstName: 'Raji’, lastName: ‘K’,
roles: ['USER'] },
{ id:1, email: 'mark_l@email.com', firstName: 'Mark’, lastName: ‘L’,
roles: ['USER', 'HOUSE ADMIN'] }
];
1.3 Request Handlers
1.3.1 GET Request without params
Following API lists all users -
// Code snippet 4 – src/mocks/handlers.js
http.get('/api/users/list', () => {
return HttpResponse.json({users})
}),
This returns a response with status success (200) and a response object containing the list of users.
1.3.2 GET Request with path params
We can fetch a specific user using following code -
// Code snippet 5 - src/mocks/handlers.js
http.get('/api/users/:id', ({params}) => {
const { id } = params
const userArray = users.filter((item) => item.id == id);
if (userArray.length === 1) {
return HttpResponse.json({user: userArray[0]})
} else {
.
.
}
This returns a response with status success (200) and a response object containing the specified user.
In case a user is not found, it returns 404 error
1.3.3 PUT Request
API to update user’s name will be as follows -
// Code snippet 6 - src/mocks/handlers.js
http.put('/api/users/update-name/:id', async({params,request}) => {
let { id } = params
id = Number(id);
const { firstName } = await (request.json());
const userArray = users.filter((item) => item.id == id );
if (userArray.length === 1) {
// in actual API the user’s first name will be updated.
return HttpResponse.json({msg: 'Success'})
} else {
return new HttpResponse(null, {
status: 404,
statusText: 'User Not found',
})
}
}),
This will return Success if user is found and 404 error otherwise.
1.3.4 POST Request
Following API is for user login -
// Code snippet 7 - src/mocks/handlers.js
http.post('/api/users/login', async({ request }) => {
const { email, password } = await (request.json());
const userArray = users.filter((item) => item.email == email);
if (userArray.length === 1) {
const user = userArray[0];
if (user.password === password) {
return HttpResponse.json({msg: 'Success'})
} else {
return HttpResponse.json({err: 'Invalid Credentials'})
}
} else {
return new HttpResponse(null, {
status: 404,
statusText: 'User Not found',
})
}
}),
We can add any logic and return required response and status.
1.3.5 DELETE request
We can delete a user -
// Code snippet 8 - src/mocks/handlers.js
http.delete('/api/users/delete/:id', ({params}) => {
const { id } = params
const userArray = users.filter((item) => item.id == id);
if (userArray.length === 1) {
// In actual api, user will be deleted
return HttpResponse.json({msg: 'Success'})
} else {
return new HttpResponse(null, {
status: 404,
statusText: 'User Not found',
})
}
}),
Response will be as below
1.4 Calling Mock APIs
Once all the request handlers are added, we can configure a worker comprised of these request handlers.
// Code snippet 9 – src/mocks/browser.js
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers); })
worker.start()
The above file browser.js needs to be included in the entry point of the application.
// Code snippet 10 – src/index.js
require('./mocks/browser.js')
The mock APIs can be called just like any other REST API.
// Code snippet 11 – src/component/*
try {
const response = await axios.get(
"https://localhost:3000/api/users/list",
);
return response;
} catch (err) {
return err.response;
}
Once the backend APIs are ready, developer can change the API URL, test, and continue developing the application.
2. Using MSW for Testing
In case we are using MSW for testing we need to configure a request mocking server with the given Request Handlers. Creating Request Handlers will remain the same as described in Sections 1.2 and 1.3
2.1 Setting up Mock Server
So instead of adding the file browser.js we can add a file called server.js with following contents
// Code snippet 12 – src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
// This configures a request mocking server with the given request handlers.
export const server = setupServer(...handlers);
2.2 Running Mock Server
Before running the tests, we need to run the server. After completing the tests, we need to close the server. This is usually added in the src/setupTests.js file if we are using Jest for testing.
// Code snippet 13 – src/setupTests.js
import '@testing-library/jest-dom';
import { server } from './mocks/server';
// Establish API mocking before all tests.
beforeAll(() => {
server.listen();
});
// Code snippet 13 – src/setupTests.js (Contd.)
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => {
server.resetHandlers();
});
// Clean up after the tests are finished.
afterAll(() => server.close());
2.3 Running Tests
As part of the test, we can render the page which displays the list of users. Since mock server is started before running the tests, when list user API is called, mock server will return the response. And in the expect clause of the test, we can compare the response received against the mock data.
// Code snippet 14 – tests/dashboardTest.js
import { users } from '../mocks/handlers.js'
it('List of users renders correctly', async () => {
const { getAllByTestId } = renderWithRouter(
<DashBoard />
{ route: '/home', path: '/home' }
);
const usersList = await waitFor(
() => getAllByTestId('users').map((p) => p.textContent),
{ timeout: '3000' }
);
expect(userList).toEqual(users);
});
3. Using MSW from an Http Server
So far in both Sections 1 and 2 the Request Handlers and Service Worker that intercepts the request was part of the application. However, for more complex applications, we might need to spawn a separate mocking server. For this purpose, the package mswjs/http-middleware
was created.
Using this package and the Request Handlers created in Section 1.2 and 1.3, we can create an actual HTTP server.
3.1 Create node application
We need to create an application and install following dependencies.
// Code snippet 15 - terminal
npm init -y
npm install express msw @mswjs/http-middleware --save-dev
3.2 Create server
Next, we need to use the createServer function and add the Request Handlers.
// Code snippet 16 – server.js
import { createServer } from "@mswjs/http-middleware";
import handlers from "../mocks/handlers.js";
const httpServer = createServer(…handlers);
httpServer.listen(9002);
After this we need to run the server, as well as the frontend application. To run the server
// Code snippet 17 - terminal
node server.js
3.3 Add Request Handlers to an existing server
If we already have a server running, we can add our Request Handlers using the createMiddleware function.
// Code snippet 18
import { createMiddleware } from "@mswjs/http-middleware";
import handlers from "../mocks/handlers.js";
import { app } from ".app";
app.use(createMiddleware(...handlers));
All the requests coming to the server will now be handled by the Request Handlers.
Conclusion
Thus, we see that there are many ways in which the MSW mocking library can be utilized. It can be used during prototyping, to checkout various API responses. Then the requirement to the backend team can be finalized. Also, since it is very easy to set up and open source, it is widely used across projects. In addition to what has been discussed above, there are many additional ways in which request handlers can be configured. Please read the official documentation mocking-responses to know more.
Many thanks to Artem Zakharchenko, who authored the Mock Service Worker around 5 years ago. Thanks for Reading and Happy Mocking!