Testing React Components with MSW

Introduction

One of the best practices in testing React components is to test them in a way that closely mimics real user interactions. This ensures that the tests are more reliable and realistic, as they reflect how the user would interact with the application.

However, relying heavily on mocks can introduce issues. Overusing mocks can obscure errors when integrating with other parts of the system (like APIs or services), and it can also make tests harder to read and maintain, as the mocked code becomes less intuitive and harder to follow.

This is where MSW (Mock Service Worker) shines. MSW allows you to mock external API calls in a way that simulates real-world conditions, effectively isolating the front-end from its external dependencies. By doing so, you can write tests that focus on the main component’s behavior without worrying about whether it depends on hooks, external services, or collaborators. This abstraction simplifies test maintenance and ensures that the component behaves as expected with all its parts working together.

Assumptions and Scope

For this guide I’m assuming you already know what are the technologies used here, and I will not try to explain them. It is only about making your project run with MSW, and how to write an initial test with it.

Jest Configuration for MSW

Jest, by default, hides Node.js globals, making certain APIs unavailable during tests. To ensure that MSW works correctly within our testing environment, we’ll configure Jest to expose the necessary Node.js components.

In the following sections, we’ll configure a Next.js project and demonstrate how to write unit tests using Jest, MSW, SWR, and React:

  • Install undici:
npm i undici
  • Create a file called jest.polyfills.js and copy the following code (it has to be in this exact order):
const { TextDecoder, TextEncoder } = require('node:util')
const { ReadableStream } = require("node:stream/web");
 
Object.defineProperties(globalThis, {
  TextDecoder: { value: TextDecoder },
  TextEncoder: { value: TextEncoder },
  ReadableStream: { value: ReadableStream },
})
 
const { Blob, File } = require('node:buffer')
const { fetch, Headers, FormData, Request, Response } = require('undici')
 
Object.defineProperties(globalThis, {
  fetch: { value: fetch, writable: true },
  Blob: { value: Blob },
  File: { value: File },
  Headers: { value: Headers },
  FormData: { value: FormData },
  Request: { value: Request },
  Response: { value: Response },
})
  • Add jest.polyfills.js to the setup files section of your jest.config.ts file:
import type {Config} from 'jest';
import nextJest from 'next/jest.js';

const createJestConfig = nextJest({
  dir: './',
})

const config: Config = {
  clearMocks: true,
  collectCoverage: true,
  coverageDirectory: "coverage",
  coverageProvider: "v8",
  setupFiles: ['./jest.polyfills.js'], // jest polyfills setup
  testEnvironment: "jsdom",
  testEnvironmentOptions: {
    customExportConditions: [''],
  },
};

export default createJestConfig(config)

Writing our unit tests

Now we’re ready to write some tests using MSW. Let’s start by defining our MSW api handlers to simulate our back-end:

  • Create a file called handlers.ts and copy the following code there:
import { http, HttpResponse } from 'msw'
import { addSubscriber, getSubscribers } from '@/lib/services/subscribers';
import { Subscriber } from '@/lib/models/subscriber';

export const handlers = [
  http.get('/api/subscribers', async () => {
    return HttpResponse.json(await getSubscribers())
  }),

  http.post('/api/subscribers', async ({request}) => {
    const subscriber = await request.json() as Subscriber;
    await addSubscriber(subscriber);
    return HttpResponse.json("ok", {status: 201})
  }),
]
  • Create our component test file and add the following code to it:
import { render, waitFor } from "@testing-library/react";
import Subscribers from "./Subscribers";
import { handlers } from "../../tests/msw/handlers";
import { setupServer } from "msw/node";
import userEvent from "@testing-library/user-event";

export const server = setupServer(...handlers);

describe("Subscribers", () => {
  beforeAll(() => {
    server.listen();
  });

  afterAll(() => {
    server.close();
  });

  it("should add new subscriber", async () => {
    const renderResult = render(<Subscribers />);

    // Wait for page to load
    await waitFor(() =>
      expect(renderResult.getByText("New subscriber")).toBeDefined(),
    );
    userEvent.click(renderResult.getByText("New subscriber"));

    // Wait for dialog to open
    await waitFor(() =>
      expect(renderResult.getByText("Add new subscriber")).toBeDefined(),
    );

    // Type full name
    await userEvent.type(
      renderResult.getByRole("textbox", { name: /full name/i }),
      "Domício Medeiros",
    );

    // Type email
    await userEvent.type(
      renderResult.getByRole("textbox", { name: /email/i }),
      "domicio@email.com",
    );

    userEvent.click(renderResult.getByText(/save/i));

    await waitFor(() =>
      expect(renderResult.queryByText("Add new subscriber")).toBeNull(),
    );

    const newRow = renderResult.getByRole('row', {  name: /domício medeiros domicio@email\.com active/i})
    expect(newRow).toBeInTheDocument();
  });
});

Pay close attention to these lines. They are responsible for starting the MSW server and specifying the handlers we defined above:

import { handlers } from "../../tests/msw/handlers";

export const server = setupServer(...handlers);

describe("useSubscribers", () => {
  beforeAll(() => {
    server.listen();
  });

  afterAll(() => {
    server.close();
  });
  
  // test cases
});

Conclusion

Aside from the beforeAll() and afterAll() setup, you won’t need to include any mock configurations in your test files. This approach makes your tests more reliable by focusing on real interactions and keeps the code cleaner, more readable, and easier to maintain for you and your team.

The full code sample is available on my GitHub page: https://github.com/dominsights/my-msw-swr-app


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *