Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

Mock Service WorkerでAPIをモックして開発をスムーズに進められた話

こんにちは。開発本部 認証連携チームでエンジニアをしている id:ruru8net です。前回はこちらの記事を書かせていただきました。
tech.classi.jp

現在は認証基盤再建というプロジェクトの中で、主にフロントエンド開発を担当しています。この記事ではフロントエンド開発においてAPIのモックのために「Mock Service Worker」を使ったところスムーズに開発を進めることができたので、使い方を紹介したいと思います。
mswjs.io

ツールの導入

弊社ではフロントエンドのフレームワークにAngularを採用しているのでAngularでの導入手順を記します。
基本的にはドキュメントの手順通りです。

1. インストール

$ npm install msw --save-dev
# or
$ yarn add msw --dev

2. モックを定義

src/mocks/handlers.tsを作成し、ファイル内にモックしたいAPIを定義します。

$ mkdir src/mocks
$ touch src/mocks/handlers.ts

今回はREST APIをモックするようにするので以下のように定義します。
src/mocks/handler.ts

import { rest } from 'msw';
    
export const handlers = [
  rest.get('/status', (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json({
        success: true
      })
    );
  }),
];

3. Mock ServiceWorker CLIのinitコマンドを実行

以下のコマンドを実行します。
(使うフレームワークによってディレクトリのパスが異なります。他のフレームワークの場合はこちら https://mswjs.io/docs/getting-started/integrate/browser#where-is-my-public-directory )

$ npx msw init ./src --save

コマンドを実行するとsrc配下に mockServiceWorker.jsが生成されます。
またAngularの場合はドキュメントに、

and add it to the assets of the angular.json file

とあるので、その通りにangular.jsonのassetsにmockServiceWorker.jsを追加します。

angular.json

{
  --- 省略 ---
     "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
           ...,
            "assets": [
              "src/favicon.ico",
              "src/assets",
+             "src/mockServiceWorker.js",
              "src/robots.txt"
            ],

4. workerを定義

src/mocks/browser.ts を作成し、workerを構成します。
https://mswjs.io/docs/getting-started/integrate/browser#configure-worker

$ touch src/mocks/browser.ts

src/mocks/browser.ts

// src/mocks/browser.js
import { setupWorker } from 'msw';
import { handlers } from './handlers';

 // This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers);

5. workerを起動

https://mswjs.io/docs/getting-started/integrate/browser#start-worker
src/main.ts内にて開発環境でのみworkerを起動するよう定義します。
弊社ではAPI用のURL(apiBaseUrl)がenvironmentに定義されていない場合にworkerを起動するようにしました。
またplatformBrowserDynamic().bootstrapModule(AppModule);よりも前にworkerが起動されていないと、ngOnInit()などで行われるAPI呼び出しがworker起動よりも先に行われNot Foundとなってしまったので、workerの起動が完了してからplatformBrowserDynamic().bootstrapModule(AppModule);を実行させるようにしています。

src/main.ts

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
    
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
    
if (environment.production) {
  enableProdMode();
}

async function prepare() {
  if (environment.apiBaseUrl === '') {
    const { worker } = await import('./mocks/browser');
    return await worker.start();
  }
  return;
}

// woker起動であるprepare()が完了してからplatformBrowserDynamic().bootstrapModule(AppModule);を実行する
prepare()
  .then(() => {
    return platformBrowserDynamic().bootstrapModule(AppModule);
  })
  .catch((err) => console.error(err));

6. モックが正しくできているかの確認

  • yarn startなどでサーバーを起動して開発者コンソールを確認します。workerが正しく起動されている場合、以下が表示されます。

    f:id:ruru8net:20220329122725p:plain
    workerが正しく起動した場合のコンソール表示

  • モックしているAPIが呼ばれる度にコンソールに表示されるので、モックの内容が正しいかどうかもコンソールにて確認ができます。

    f:id:ruru8net:20220329122914p:plain
    モックが正しく呼ばれた場合のコンソール表示

  • 404が出る場合は、正しくモックできていない可能性が高いので、src/mocks/handler.tsを見直したり、APIのパスが誤っていないかを確認します。

    f:id:ruru8net:20220329122924p:plain
    モックが呼び出せず404エラーになった場合のコンソール表示

便利だったオプション

Mock Service Workerには様々なオプションがあります。
ドキュメントが丁寧なのでそちらに記載されているのですが、その中でも自分が実際に使い、便利だなと思ったものを紹介します。

request.bodyでの分岐

以下のようにrequest内のbodyを見て、処理を分岐することができます。
これによって正常系や異常系の分岐を簡単におこなうことができました。

src/mocks/handler.ts

rest.post('/login', (req, res, ctx) => {
  const { username } = req.body as { username: string };

  if (username === 'unknown') {
    return res(
      ctx.status(404),
      ctx.json({
        success: false,
        errors: {
          message: 'このユーザは存在しません。',
        },
      })
    );
  }
    
  return res(
    ctx.status(200),
    ctx.json({
      success: true
    })
  );
}),

応答時間を遅らせる

delay() を使うことで応答までの時間を指定秒遅らせることができます。
https://mswjs.io/docs/api/context/delay
レスポンスを受け取ってから処理をするといったような同期的な実装が正しく動作しているかを確認するのに便利でした。

src/mocks/handler.ts

rest.post('/status', (req, res, ctx) => {
  return res(
    // 1000ms = 1s 遅らせる.
    ctx.delay(1000),
    ctx.status(200),
    ctx.json({
      success: true
    })
  )
})

ネットワークエラーを返す

以下のように書くことで、ネットワークエラーとしてレスポンスを返すことができます。
https://mswjs.io/docs/api/response/network-error

src/mocks/handler.ts

rest.get('/status', (req, res, ctx) => {
  return res.networkError('Failed to connect')
})

コンソールではこのように表示されます。

f:id:ruru8net:20220329121210p:plain
MSWでnetworkError()をreturnした場合のコンソール表示

ネットワークエラーのハンドリングを実装する際にとても便利でした。

Mock Service Workerを使った開発でのメリット

サーバサイドに依存しない

今回携わっていたプロジェクトではサーバサイドとフロントエンドでリポジトリも異なり、それぞれ別の人が開発をしていました。 また開発の進め方として、サーバサイドの開発者と一緒にAPIを決定し、定義書にまとめ、その後の開発はそれぞれで進めていくという流れでした。
フロントエンド開発ではMock Service Workerを使えばいくらでもAPIを検証することができ、サーバサイドの開発進捗に依存することなく進められたことが一番良かったです。
開発が進む中で何度かAPIの仕様が変わることもありましたが、src/mocks/handler.ts を修正すれば済むので、APIの追加や修正に負担もなくとても開発が進めやすかったです。

ユーザーエラーの再現のしやすさ

ネットワークエラーはユーザーによく起こりうるものでありながら、開発環境ではなかなか再現がしづらいと感じていました。しかし先程紹介したようにオプションで簡単に定義することができます。その他のエラーも開発環境での再現ができるのでハンドリングのしやすさも大きなメリットの1つだと思います。

型定義を通して実際のレスポンス形式と同期が取れる

handler.tsにモックを定義する際のレスポンスにも、実際のAPIのレスポンスで使用している型をimportすることで、APIの変更があった場合にモックの修正にも気がつくことができます。同期が取れるように型を意識しておくことも大事だと思います。

api.service.ts

type GetStatusResponse = {
  success: true;
  status: string;
};

async getStatus(): Promise<LoginMethod[]> {
  return await this.http
    .get<GetStatusResponse>(`/status`)
    .toPromise()
    .catch(error => console.log(error));
}

src/mocks/handler.ts

import { rest } from 'msw';
import { GetStatusResponse } from 'api.service.ts'

export const handlers = [
  rest.get('/status', (req, res, ctx) => {
    const response: GetStatusResponse = {
      success: true,
      status: 'hoge',
    }
    return res(
      ctx.status(200),
      ctx.json(response)
    );
  }),
];

Mock Service Workerを使った開発でのデメリット

他のモックの手法を使ったことがなく、他との比較はできないのですが困ったところは今のところ全くないです。
もし今後新たな開発をすることになっても Mock Service Workerを採用したいと思っています。

今後やっていきたいこと

モックAPIの定義ファイルのリファクタ

src/mocks/handler.ts に全てのAPIを羅列してしまうとファイルの行数が膨大になり、可読性が下がります。 ファイルをいくつかに切り分け、APIを種類ごとに分類するといったようなリファクタが必要であると思い、現在試行錯誤しています。
できる限りAPI定義書と合わせて見た際に理解がしやすい構成にしたいです。

まとめ

弊社では毎週「フロントエンド互助会」といった各プロジェクトのフロントエンド開発をしているエンジニアが集まり、進捗共有や相談を行う時間があります。
今回、そこでMock Service Workerについて共有したところ、「便利そう」「使って見たい」と言った声や、実は他のプロジェクトでも使っていて、そこではこんなファイル構成をしているよ、といったお話を聞くことができました。またこのブログを執筆する機会もこちらで提案してもらい、書かせていただいてます。
まだまだ社内でのMock Service Workerの浸透率は高くないので、次の一歩として布教活動と今後Mock Service Worker使っていくというプロジェクトがあれば積極的にサポートをしていきたいです。

© 2020 Classi Corp.