Mock Service Worker (MSW) を使ったAPIモックとテストの効率化

  • testinglibrary
    testinglibrary
  • jest
    jest
  • typescript
    typescript
  • react
    react
2023/09/25に公開

はじめに

本記事では、フロントエンド開発やテストにおいて非常に便利なツールである「Mock Service Worker (MSW)」について解説します。MSWを使用することで、バックエンドのAPIが未完成でもフロントエンドの開発を進めることができ、また、テストの安定性を向上させることができます。まずはMSWの基本的な使い方を紹介し、その後、MSWを使用しない場合のテストコードとどう違うかの比較を含めどのように活用できるかを詳しく説明していきます。

今回のゴール

データをフェッチするReactコンポーネントの自動テストを用いてMSWが有効である検証を行います。

最初はJestのモックを使って自動テストを行うコードを書きます。
その後、JestのモックをMSWに置き換えることでテストコードが簡単に書けるようになることを比較しながらご紹介します。
MSWサーバーの起動方法なども併せてご紹介しておりますので、この記事を参考に現場で実践できるようになれば幸いです。

Mock Service Worker(MSW)とは?

MSW(Mock Service Worker) は、API のモック(仮のサーバーレスポンスを作成) をするためのライブラリです。
ブラウザや Node.js 環境で動作し、フロントエンド開発やテストの際に 実際の API を呼び出さずにモックレスポンスを返せる のが特徴です。

主な用途

1.フロントエンド開発時にバックエンドが未完成でも作業できる
実際の API がまだ用意されていなくても、モックデータを返せるので開発が進められる。
API の設計変更があっても、すぐにモックデータを更新できる。

2.テストの安定化
Jest / Playwright / Cypress などのテストで API をモックすることで、外部 API の影響を受けずに安定したテストが可能。
ネットワークエラーや特定のレスポンスを簡単にシミュレーションできる。

3.ネットワークリクエストをフックし、開発中のデバッグを楽にする
MSW を使うと、実際にリクエストがどんなデータを送受信しているか確認しやすい。
例えば、API からのレスポンスをカスタマイズしてエラーハンドリングの動作をチェックできる。

記事一覧を表示するコンポーネントでテスト

今回はブログ記事の一覧を表示するコンポーネントを用意しテストコードの実装を検討してみます。

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>記事がありません</p>
      )}
    </div>
  )
}

画面を起動するとデータ取得の処理が動きます。
データがあった場合はuseStateでデータを格納しその結果を表示するというコンポーネントになります。

このコンポーネントを使って画面に表示する処理となります。

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

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

この設定で先ほどの記事一覧のコンポーネントをブラウザで確認できますので一度、Next.jsを起動して確認します。

npm run dev

画面には「記事がありません」と出ておりコンソールログにはfetchで404 Not Foundのエラーが出ています。

Image from Gyazo

テストコードの実装

まずはMock Service Workerを使わずに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('ブログ記事一覧を表示する', async () => {
    const mockArticles = [
      { id: 1, title: '1件目の記事', description: 'これはテストです。' },
      { id: 2, title: '2件目の記事', description: 'これもテストです。' }
    ];

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

    // コンポーネントをレンダリング
    render(<Articles />);

    expect(screen.getByText('記事がありません')).toBeInTheDocument();

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

      expect(screen.getByText('1件目の記事')).toBeInTheDocument();
      expect(screen.getByText('2件目の記事')).toBeInTheDocument();
    });
  });
});

テストコードの解説になります。

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

global.fetchはブラウザ環境におけるfetchのデフォルトのインスタンス。
jest.fn()でモック化して、mockResolvedValueOnceやmockRejectedValueOnceを使い、リクエストに応じたレスポンスを定義できます。

    const mockArticles = [
      { id: 1, title: '1件目の記事', description: 'これはテストです。' },
      { id: 2, title: '2件目の記事', description: 'これもテストです。' }
    ];

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

mockResolvedValueOnceを使い、json関数の戻り値として期待するデータを返します。

    expect(screen.getByText('記事がありません')).toBeInTheDocument();

起動したタイミングでは記事データが存在しないため「記事がありません」と表示されることを確認します。

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

      expect(screen.getByText('1件目の記事')).toBeInTheDocument();
      expect(screen.getByText('2件目の記事')).toBeInTheDocument();
    });

記事データがセットされた後の状態を下記の2件で確認します。

  • liタグが2つ存在すること
  • それぞれの記事タイトルが表示されること

テストコードができましたので実際にテストを実施します。

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

Image from Gyazo

Mock Service Workerを使わずともfetchをモックに置き換えることでコンポーネントのテストができました。

多くのケースではこれで十分かもしれません。

ただよりテストの実装をシンプルにし工数を抑えたいという方々向けにMock Service Worker(MSW)をご紹介したいと思います。

Mock Service Workerのセットアップ

mswをプロジェクトにインストールします。

npm install msw --save-dev

MSWハンドラーを作成

APIリクエストのモックレスポンスを定義するハンドラーを作成します。

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

export const handlers = [
  http.get('/api/articles', () => {
    return HttpResponse.json([
      { id: 1, title: '1件目の記事', description: 'これはテストです。' },
      { id: 2, title: '2件目の記事', description: 'これもテストです。' }
    ])
  })
];

MSWサーバーを設定

テスト用のMSWサーバーを設定します。

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

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

JestセットアップでMSWを起動

テストが始まる前にMSWサーバーを起動し、終了時にクリーンアップします。

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

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

Jestの設定でjest.setup.tsを読み込むようにします。

※前回の記事と同じ環境でしたら既に設定してあるかと思うので対応不要です。

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

JestでNode.jsを動かすために

jest.config.tsでjestをブラウザ環境で動かすことを想定した設定を入れています。

jest.config.ts
const config: Config = {

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

そのため現状の設定でJestからMSWを起動するとエラーが発生します。

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

公式ドキュメントに従いjest-fixed-jsdomを導入します。

npm i -D jest-fixed-jsdom

jest.config.tsで設定変更します。

jest.config.ts
const config: Config = {

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

テストコードの修正

Jestでテストをする際にMSWが起動しているためfetchをそのままコンポーネント側で動かせるようになります。

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('ブログ記事一覧を表示する', async () => {
-   const mockArticles = [
-     { id: 1, title: '1件目の記事', description: 'これはテストです。' },
-     { id: 2, title: '2件目の記事', description: 'これもテストです。' }
-   ];

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

    // コンポーネントをレンダリング
    render(<Articles />);

    expect(screen.getByText('記事がありません')).toBeInTheDocument();

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

      expect(screen.getByText('1件目の記事')).toBeInTheDocument();
      expect(screen.getByText('2件目の記事')).toBeInTheDocument();
    });
  });
});

モックがなくなった分、見やすいテストコードになりました。

下記コマンドでテストを実行し正常終了することを確認します。

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

さいごに

Mock Service Worker(MSW)は、フロントエンド開発やテストの効率化に非常に役立つツールです。実際のAPIリクエストをモックすることで、開発中に発生しがちな問題を軽減し、テストの信頼性を高めることができます。MSWを活用することで、バックエンドが完成していなくてもフロントエンドの開発が進められ、テストもスムーズに行うことが可能です。この記事を参考に、MSWをあなたのプロジェクトに取り入れて、開発効率をさらに向上させましょう。

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

記事に関するお問い合わせ📝

記事の内容に関するご質問、ご意見などは、下記よりお気軽にお問い合わせください。
ご質問フォームへ

技術支援などお仕事に関するお問い合わせ📄

技術支援やお仕事のご依頼に関するお問い合わせは、下記よりお気軽にお問い合わせください。
お問い合わせフォームへ

関連する技術ブログ

弊社の技術支援サービス

お問い合わせ

経営と現場をつなぐ“共創型”の技術支援。
成果に直結するチーム・技術・プロセスを共に整えます。

お問い合わせ