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:

server.js
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 ...:

handlers.js
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:

setupTests.js
// 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:

App.test.js
  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:

App.test.js
  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:

App.test.js
  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:

App.test.js
  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:

App.test.js
  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

App.test.js
  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

React Testing Library

Testing React application with testing-library

Resources

Source Code.