Testing React application with Mock Service Worker (mswjs)
Author : JaNakh Pon , February 09, 2022
Tags
Mock Service Worker (API mocking of the next generation)
Mock by intercepting requests on the network level. Seamlessly reuse the same mock definition for testing, development, and debugging.
Comparison , Getting Started , MSW Examples
Intro
In this article, we are gonna test the HTTP requests by mocking the request, response with MSW. The reason we are using MSW is simple, it's because MSW will mock by intercepting requests on the network level and we don't have to write seperate mocking requests,responses for axios and fetch individually.
For backend, we will reuse a plain nest.js todo api that we built months ago and for frontend we will reuse some code blocks from our Framer Motion article.
NOTE : we will write mock tests only for REST API with plain JavaScript this time and there will soon be articles about mocking GraphQL API with JavaScript and Typescript ✌️.Setup
"@testing-library/react" is included in create-react-app
by default so, we will just use CRA with TailwindCSS and I will reuse UI Layout from our previous Framer Motion Article.
And just like our previous article, let's start by creating __tests__
folder under /src/
and create App.test.js
as a test suite/file for App.js
.
To set up MSW, install msw to devDependencies:
> npm install msw --save-dev || yarn add msw --dev
And create mocks
folder under /src
and will create server
and handler
files under it:
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
and we will mock requests,responses in handlers, read more here about handler, resolver ...:
import { rest } from "msw";
export const handlers = [
rest.post("http://localhost:3001/api/v1/todos", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
title: "Hitoribochi",
text: req.body.text,
completed: false,
id: 3,
created_at: "2022-02-07T17:57:18.950Z",
updated_at: "2022-02-07T17:57:18.950Z",
})
);
}),
rest.get("http://localhost:3001/api/v1/todos", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
{
id: 1,
title: "Friday",
text: "mock testing text 1",
completed: false,
created_at: "2022-02-03T22:50:51.570Z",
updated_at: "2022-02-03T22:50:51.570Z",
},
{
id: 2,
title: "Friday",
text: "mock testing test 1",
completed: false,
created_at: "2022-02-03T22:03:31.858Z",
updated_at: "2022-02-03T22:03:31.858Z",
},
])
);
}),
rest.put("http://localhost:3001/api/v1/todos/1", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
"id": 1,
"title": "Hitoribochi",
"text": "Hitoribochi Text",
"completed": true,
"created_at": "2022-02-08T07:16:39.686Z",
"updated_at": "2022-02-08T07:17:07.556Z"
})
);
}),
rest.delete("http://localhost:3001/api/v1/todos/1", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
message: "successfully removed task with id: 1",
})
);
}),
];
and now, we need to orchestrate how our mock server will behave in setupTests.js (explained with comments) and read more about it here:
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import { server } from './mocks/server';
// Establish requests interception layer before all tests.
beforeAll(() => server.listen())
// Removes any request handlers that were added on runtime (after the initial setupServer call).
// This function accepts an optional list of request handlers to override the initial handlers to re-declare the mock definition completely on runtime.
afterEach(() => server.resetHandlers())
// Clean up after all tests are done, preventing this
// interception layer from affecting irrelevant tests.
afterAll(() => server.close())
Since we have our mock server configured and ready, let's start writing test cases for our App's CRUD functionalities.
case 1: let's write a case where our TaskList
should be in DOM after rendering <App />
Component:
test("TaskList should be in DOM", async () => {
render(<App />);
const taskListElement = screen.getByRole("list");
await waitFor(() => expect(taskListElement).toBeInTheDocument());
});
case 2: we put 2 Tasks as a response for GET
endpoint in our handers, so our TaskList should have 2 two once the component is ready:
test("Task list should have 2 list items", async () => {
render(<App />);
const taskListElement = screen.getByRole("list");
const { getAllByRole } = within(taskListElement);
await waitFor(() => expect(getAllByRole("listitem").length).toBe(2));
});
case 3: If the user typed something in textbox and press enter, it should be added to task list:
test("Newly added task should be in the TaskList", async () => {
render(<App />);
const textboxElement = screen.getByRole("textbox");
userEvent.type(textboxElement, "testing text");
expect(textboxElement).toHaveDisplayValue("testing text");
fireEvent.keyDown(textboxElement, {
key: "Enter",
code: "Enter",
keyCode: 13,
charCode: 13,
});
const taskListElement = screen.getByRole("list");
const { getAllByRole } = within(taskListElement);
await waitFor(() => expect(getAllByRole("listitem").length).toBe(1));
const addedItemHeading = screen.getByRole("heading", {
name: /testing text/i,
});
expect(addedItemHeading).toBeInTheDocument();
});
case 4: If the user clicked delete button of the task, that task should be removed from the task List:
test("TaskList should be decreased after clicking delete button", async () => {
render(<App />);
const taskListElement = screen.getByRole("list");
const { getAllByRole } = within(taskListElement);
await waitFor(() => expect(getAllByRole("listitem").length).toBe(2));
const deleteButton = screen.getByTestId("deleteButton0");
expect(deleteButton).toBeInTheDocument();
userEvent.click(deleteButton);
await waitFor(() => expect(getAllByRole("listitem").length).toBe(1));
});
case 5: If the user clicked completed button of the task, that task should be updated with "Completed" text:
test("Task Item should be updated after clicking complete button", async () => {
render(<App />);
const completeButton = await screen.findByTestId("completeButton0");
expect(completeButton).toBeInTheDocument();
userEvent.click(completeButton);
expect(await screen.findByText("Completed")).toBeInTheDocument();
expect(await screen.findByTestId("listItemCompleted0")).toHaveTextContent(
"Completed"
);
});
act() issue
Warning: An update to App inside a test was not wrapped in act(...).
Sometimes, when we write test cases related to upating state
, we might run into warning that says :
"Warning: An update to App inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => { / fire events that update state / }); / assert on the output / "
For example, in our test case 5, if we change it to
test("Task Item should be updated after clicking complete button", async () => {
render(<App />);
const completeButton = await screen.findByTestId("completeButton0");
expect(completeButton).toBeInTheDocument();
userEvent.click(completeButton);
const listItem = screen.getByTestId("listItemCompleted0");
expect(listItem).toBeInTheDocument();
});
now this changes will give you warning about "act()".
However, this issue can simply be solved by using async methods from RTL such as findBy Queries and waitFor. To
You Probably Dont Need act() in Your React Tests
References
Bruno Antunes - Mock HTTP calls using Fetch or Axios - Mock Service Worker
Related Articles
Testing React application with testing-library