Loading
BLOG 開発者ブログ

2021年12月25日

Mock Service Worker を使ってみた

まだバックエンド API の開発が終わっていないけど、API 呼び出しを伴う画面の動作を確認したい…
そんな時に便利なライブラリ Mock Service Worker を TypeScript で使ってみましたので、その内容をご紹介します。

目次

はじめに

この記事は アイソルート Advent Calendar 2021 25日目の記事です。

こんにちは。クラウドソリューション第一グループの namiki.t です。

昨日は akahane.t さんの 「RDPが使えなくてもリモートデスクトップ接続する」でした。
「え、どうやってやるの!?」と少しでも気になった方も、気にならなかった方も、ぜひこちらの記事もご覧ください。

Mock Service Worker とは

Mock Service Worker は、ネットワークレベルでリクエストをインターセプトし、Mock データを返すライブラリです。

名前にもある通り、実態は Service Worker で、これを使用することで実際のバックエンド API や Mock サーバを立てることなく、フロントエンドアプリケーションの開発やテストを行うことが可能です。

2021年12月25日現在、REST および GraphQL に対応しています。

使ってみた

今回は、Todo リストを題材に

  • 作成
  • 取得
  • 更新
  • 削除

の処理を行う API の Mock を Mock Service Worker (TypeScript) で作成した内容をご紹介します。

本記事では、下記を前提条件とします。

  • create-react-app (以下 CRA) で作成された React (TypeScript) プロジェクトに Mock Service Worker を導入する
  • API 呼び出しを行うサービスクラスおよびそれを使用する画面 (コンポーネント) は作成済み
  • 記事の内容は 2021年12月25日 時点のものであり、各種 API の使用方法が変更される可能性がある

また、今回使用した主な npm パッケージの情報は下記の通りです。

パッケージ名 バージョン
react 17.0.2
react-dom 17.0.2
react-scripts 4.0.3
typescript 4.1.2
axios 0.24.0
msw 0.36.3

上記を踏まえ、下記の手順で説明します。

  1. Mock Service Worker をインストールする
  2. Service Worker を生成する
  3. Mock API の処理内容を作成する
  4. handlers.ts を作成する
  5. browser.ts を作成する
  6. worker.start() を呼び出す処理を追記する
  7. 動作確認する

1. Mock Service Worker をインストールする

はじめに、以下のコマンドで Mock Service Worker の npm パッケージをインストールします。
開発時にのみ使用するため、-D オプションを使用します。

$ npm install -D msw

2. Service Worker を生成する

次に、以下のコマンドで Mock Service Worker の Service Worker を生成します。
第3引数には、プロジェクトのパブリックディレクトリへの相対パスを指定します。
今回は CRA で作成された React アプリケーションを導入先としているため、
以下コマンドをプロジェクトフォルダ直下で実行します。

$ npx msw init ./public --save

成功すると、指定したパブリックディレクトリに mockServiceWorker.js というファイルが追加されているはずです。

導入先アプリケーションによってパブリックディレクトリが異なるため、
指定する内容は公式ドキュメント(init – Cli – Mock Service Worker Docs)の内容を参考にしてください。

3. Mock API の処理内容を作成する

前準備が整いましたので、Mock API の処理内容を作成していきます。
今回作成するのは、以下のサービスクラスからのリクエストに対する Mock API です。

import axios from 'axios';
import { API } from 'environment';
import { Todo, TodoCreateDto, TodoUpdateDto } from 'models';

export class TodoService {
  async fetchAll(): Promise<Todo[]> {
    return (await axios.get<Todo[]>(`${API}/todos`)).data;
  }

  async create(todo: TodoCreateDto): Promise<Todo> {
    return (await axios.post<Todo>(`${API}/todos`, todo)).data;
  }

  async update(id: number, todo: TodoUpdateDto): Promise<TodoUpdateDto> {
    return (await axios.put<TodoUpdateDto>(`${API}/todos/${id}`, todo)).data;
  }

  async remove(id: number): Promise<number> {
    return (await axios.delete<number>(`${API}/todos/${id}`)).data;
  }
}

ポイントはハイライトがかかっている部分です。

import { API } from 'environment';
import { Todo, TodoCreateDto, TodoUpdateDto } from 'models';
import { DefaultRequestBody, PathParams, rest } from 'msw';

const mockData: Todo[] = [];

class MockTodoRepository {
  constructor(private data: Todo[]) {}

  fetchAll(): Todo[] {
    return this.data;
  }

  create(todo: TodoCreateDto): Todo {
    const id = (this.data.length > 0 ? Math.max(...this.data.map((v) => v.id)) : 0) + 1;
    const createdTodo = { ...todo, id, completed: false };
    this.data = [...this.data, createdTodo];
    return createdTodo;
  }

  update(id: number, todo: TodoUpdateDto): TodoUpdateDto {
    this.data = this.data.map((v) => (v.id !== id ? v : { ...v, ...todo }));
    return todo;
  }

  remove(id: number): number {
    this.data = this.data.filter((v) => v.id !== id);
    return id;
  }
}

const mockTodoRepository = new MockTodoRepository(mockData);

/**
 * 第1型引数: リクエストボディの型を指定
 * 第2型引数: デフォルト型の PathParams を指定
 * 第3型引数: レスポンスボディの型を指定
 */
export const todoHandlers = [
  rest.get<DefaultRequestBody, PathParams, Todo[]>(`${API}/todos`, (_, res, ctx) => {
    return res(ctx.status(200), ctx.json(mockTodoRepository.fetchAll()));
  }),
  rest.post<TodoCreateDto, PathParams, Todo>(`${API}/todos`, (req, res, ctx) => {
    return res(ctx.status(201), ctx.json(mockTodoRepository.create(req.body)));
  }),
  rest.put<TodoUpdateDto, PathParams, TodoUpdateDto>(`${API}/todos/:id`, (req, res, ctx) => {
    const { id } = req.params;
    return res(ctx.status(200), ctx.json(mockTodoRepository.update(+id, req.body)));
  }),
  rest.delete<DefaultRequestBody, PathParams, number>(`${API}/todos/:id`, (req, res, ctx) => {
    const { id } = req.params;
    return res(ctx.status(200), ctx.json(mockTodoRepository.remove(+id)));
  }),
];

作成 → 取得 → 更新 → 削除 の一連の流れを動作させるために Mock の Repository を頑張って作っていますが、単純に適当なレスポンスを返すのが目的であればここまでする必要はありません。

4. handlers.ts を作成する

前段で作成した handler を1か所に集約するファイルを作成します。

import { todoHandlers } from './api/todo';

export const handlers = [...todoHandlers];

5. browser.ts を作成する

前段で作成した handlers.ts を読込み、worker を生成するファイルを作成します。

import { setupWorker } from 'msw';

import { handlers } from './handlers';

export const worker = setupWorker(...handlers);

6. worker.start() を呼び出す処理を追記する

前段で作成した browser.ts を読み込み、worker.start() を呼び出す処理を index.tsx に追記します。


import './index.css';

import React from 'react';
import ReactDOM from 'react-dom';

import { App } from './App';
import { worker } from './mocks/browser';
import reportWebVitals from './reportWebVitals';

/**
 * 開発モードでのみ動作させる
 */
if (process.env.NODE_ENV === 'development') {
  worker.start();
}

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

7. 動作確認する

これで全ての準備が整いました。動作確認をします。

アプリケーションは、初期表示時・作成時・更新時・削除時に todo の一覧を全件取得する仕様になっています。

作成した4つの Mock API の処理が動作していることが確認できました!

終わりに

今回は React (TypeScript) プロジェクトへ Mock Service Worker を導入し、その使用感を確認しました。

数ステップのセットアップと僅かなコーディングで Mock サーバを作成できるのは非常に魅力的で、
開発手法の一つとして積極的に採用していきたいと感じました。

是非ご覧いただいた皆さんのフロントエンド開発にも役立てていただければ幸いです。

namiki.tのブログ