Streamlining API Mocking and Testing with Mock Service Worker (MSW)

  • testinglibrary
    testinglibrary
  • jest
    jest
  • typescript
    typescript
Published on 2023/09/25

Introduction

This article explains “Mock Service Worker (MSW)”, a very useful tool for frontend development and testing. By using MSW, you can continue frontend development even when the backend APIs are not yet complete, and you can also improve the stability of your tests. We’ll first introduce the basic usage of MSW, then explain in detail how to leverage it, including a comparison with test code that does not use MSW.

Goal of this article

We will verify the effectiveness of MSW using automated tests for a React component that fetches data.

First, we’ll write test code that uses Jest mocks.
Then we’ll replace the Jest mocks with MSW and compare how this makes the test code easier to write.
We’ll also cover how to start the MSW server, so you can use this article as a reference to apply it in real projects.

What is Mock Service Worker (MSW)?

MSW (Mock Service Worker) is a library for mocking APIs (creating fake server responses).
It runs in both browser and Node.js environments, and its main feature is that it can return mock responses during frontend development and testing without calling the real APIs.

Main use cases

  1. Continue frontend development even when the backend is not ready
    Even if the real API is not yet available, you can return mock data and keep development moving.
    If the API design changes, you can quickly update the mock data.

  2. Stabilize tests
    By mocking APIs in tests with Jest / Playwright / Cypress, you can run stable tests without being affected by external APIs.
    You can easily simulate network errors or specific responses.

  3. Hook into network requests to make debugging easier during development
    With MSW, it’s easy to inspect what data is actually being sent and received in requests.
    For example, you can customize API responses to check how your error handling behaves.

Testing a component that displays a list of articles

This time, we’ll prepare a component that displays a list of blog articles and consider how to implement its test code.

components/articles.tsx
'use client'
import { useEffect, useState } from "react"

type Article = {
  id: number,
  title: string
  description?: string
}

export const Articles = () => {
  const [articles, setArticles] = useState<Article[] | null>(null)

  useEffect(() => {
    fetch('/api/articles')
      .then((response) => response.json())
      .then((data) => setArticles(data.message));
  }, []);

  return (
    <div>
      {articles?.length ? (
        <ul>
          {articles.map((item) =>
            <li key={item.id}>{item.title}</li>
          )}
        </ul>
      ) : (
        <p>No articles found</p>
      )}
    </div>
  )
}

When the screen loads, the data-fetching process runs.
If data exists, it is stored with useState and the result is displayed; that’s what this component does.

We then use this component to render the screen.

app/page.tsx
import { Articles } from "@/components/articles";

export default function Home() {
  return (
    <div>
      <Articles />
    </div>
  )
}

With this setup, you can check the article list component in the browser, so start Next.js once and verify it.

npm run dev

The screen shows “No articles found” (“No articles available”), and the console log shows a 404 Not Found error from fetch.

Image from Gyazo

Implementing the test code

First, we’ll run tests using fake data by mocking fetch directly, without using Mock Service Worker.

components/articles.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { Articles } from './articles';

describe('Articles Component', () => {
  beforeEach(() => {
    global.fetch = jest.fn();
  });

  afterEach(() => {
    jest.resetAllMocks();
  });

  test('Display blog article list', async () => {
    const mockArticles = [
      { id: 1, title: 'First Article', description: 'This is a test.' },
      { id: 2, title: 'Second Article', description: 'This is also a test.' }
    ];

    (global.fetch as jest.Mock).mockResolvedValueOnce({
      json: jest.fn().mockResolvedValueOnce(mockArticles),
    });

    // Render component
    render(<Articles />);

    expect(screen.getByText('No articles found')).toBeInTheDocument();

    await waitFor(() => {
      const items = screen.getAllByRole('listitem');
      expect(items).toHaveLength(2);

      expect(screen.getByText('First Article')).toBeInTheDocument();
      expect(screen.getByText('Second Article')).toBeInTheDocument();
    });
  });
});

Here is an explanation of the test code.

  beforeEach(() => {
    global.fetch = jest.fn();
  });

global.fetch is the default fetch instance in a browser environment.
By mocking it with jest.fn(), you can define responses for each request using mockResolvedValueOnce or mockRejectedValueOnce.

    const mockArticles = [
      { id: 1, title: 'First Article', description: 'This is a test.' },
      { id: 2, title: 'Second Article', description: 'This is also a test.' }
    ];

    (global.fetch as jest.Mock).mockResolvedValueOnce({
      json: jest.fn().mockResolvedValueOnce(mockArticles),
    });

We use mockResolvedValueOnce so that the json function returns the expected data.

    expect(screen.getByText('No articles found')).toBeInTheDocument();

At the moment the component starts up, there is no article data, so we verify that “No articles found” (“No articles available”) is displayed.

    await waitFor(() => {
      const items = screen.getAllByRole('listitem');
      expect(items).toHaveLength(2);

      expect(screen.getByText('First Article')).toBeInTheDocument();
      expect(screen.getByText('Second Article')).toBeInTheDocument();
    });

After the article data has been set, we verify the following two points:

  • There are two li tags
  • Each article title is displayed

Now that the test code is ready, let’s actually run the test.

npm run test -- components/articles.test.tsx

Image from Gyazo

Even without using Mock Service Worker, we were able to test the component by mocking fetch.

In many cases, this may be sufficient.

However, for those who want to simplify test implementation further and reduce effort, I’d like to introduce Mock Service Worker (MSW).

Setting up Mock Service Worker

Install msw in your project.

npm install msw --save-dev

Create MSW handlers

Create handlers that define mock responses for API requests.

mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/articles', () => {
    return HttpResponse.json([
      { id: 1, title: 'First Article', description: 'This is a test.' },
      { id: 2, title: 'Second Article', description: 'This is also a test.' }
    ])
  })
];

Configure the MSW server

Set up an MSW server for tests.

mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

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

Start MSW in the Jest setup

Start the MSW server before tests begin and clean it up afterward.

jest.setup.ts
import '@testing-library/jest-dom'
+ import { server } from './mocks/server';

+ beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
+ afterEach(() => server.resetHandlers());
+ afterAll(() => server.close());

Configure Jest to load jest.setup.ts.

If you’re using the same environment as in the previous article, this should already be configured and no changes are needed.

jest.config.ts
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],

To run Node.js with Jest

In jest.config.ts, we’ve configured Jest assuming it runs in a browser-like environment.

jest.config.ts
const config: Config = {

  clearMocks: true,
  coverageProvider: "v8",
  preset: "ts-jest",
  testEnvironment: "jest-environment-jsdom",

Because of this, starting MSW from Jest with the current settings will cause an error.

https://mswjs.io/docs/faq/#requestresponsetextencoder-is-not-defined-jest

Following the official documentation, we’ll introduce jest-fixed-jsdom.

npm i -D jest-fixed-jsdom

Update the settings in jest.config.ts.

jest.config.ts
const config: Config = {

  clearMocks: true,
  coverageProvider: "v8",
  preset: "ts-jest",
- testEnvironment: "jest-environment-jsdom",
+ testEnvironment: "jest-fixed-jsdom",

Modifying the test code

Since MSW is running when Jest executes tests, the component can now use fetch as-is.

Remove the logic that mocked fetch.

components/articles.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { Articles } from './articles';

describe('Articles Component', () => {
- beforeEach(() => {
-   global.fetch = jest.fn();
- });

- afterEach(() => {
-   jest.resetAllMocks();
- });

  test('Display blog article list', async () => {
-   const mockArticles = [
-     { id: 1, title: 'First Article', description: 'This is a test.' },
-     { id: 2, title: 'Second Article', description: 'This is also a test.' }
-   ];

-   (global.fetch as jest.Mock).mockResolvedValueOnce({
-     json: jest.fn().mockResolvedValueOnce(mockArticles),
-   });

    // Render component
    render(<Articles />);

    expect(screen.getByText('No articles found')).toBeInTheDocument();

    await waitFor(() => {
      const items = screen.getAllByRole('listitem');
      expect(items).toHaveLength(2);

      expect(screen.getByText('First Article')).toBeInTheDocument();
      expect(screen.getByText('Second Article')).toBeInTheDocument();
    });
  });
});

With the mocks removed, the test code has become easier to read.

Run the test with the following command and confirm that it finishes successfully.

npm run test -- components/articles.test.tsx

Conclusion

Mock Service Worker (MSW) is a highly useful tool for improving the efficiency of frontend development and testing. By mocking real API requests, it reduces common issues that arise during development and increases the reliability of your tests. With MSW, you can continue frontend development even before the backend is complete, and you can run tests smoothly. Use this article as a reference to introduce MSW into your projects and further improve your development efficiency.

Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form