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 yourjest.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
Leave a Reply