🐈

MSWのモック作成をもう少しだけ自動化する

2023/12/15に公開

はじめに

TUNAGのフロントエンド開発では、モックサーバーとしてMSWを採用しています。
MSWはサービスワーカーAPIを利用して、APIリクエストを捕捉し、実際のリクエストを行う代わりに、開発者が定義したモックを返すライブラリです。
これにより、APIがまだ完成していない状況でもフロントエンドの開発を進めることができ、テストにも活用できます。
MSWのモックの作成は、hygenによるテンプレートを用いて、ほとんど自動生成しているので、既に作成コストは低い状態でしたが、それでも手動で行なっている実装部分があったため、そこを自動化することで、モックの作成をさらに簡単に行えるように改善しました。

今までのモック作成までの流れ

(ライブラリのインストールや初回のみに行う設定は省略します)

1. yarn hygen generators mocks で雛形を作成

雛形の作成にはhygenを使用していて、yarn hygen generators mocksを実行し、エンドポイントを入力することで、ほとんど自動生成しています。

こちらがhygenのテンプレートの例ですが、ここに登場する examplesはOpenAPIから自動生成したモックデータであり、 restMockこちらの記事にあるようなaspida × MSWで型安全なモックを作るための関数になります。


...省略

export const mock<%= camelName %><%= capitalize(method) %> = restMock.<%= method %>(
  <%= apiClient %>.api.<%= version %>.<%= aspidaEndpoint %>,
  () => {
    const statusCode = getGetStatusCode();

    if (statusCode === 200) return HttpResponse.json(examples);

    // エラー
    return new HttpResponse(null, { status: statusCode });
});

このようにして、型安全なMSWのハンドラーを(ほぼ)自動生成しています。

2. MSWのハンドラーをsetupServer にセット

1で作成したハンドラーをMSWの setupServer の第一引数に配列で渡します。

import { setupServer } from 'msw/node';
import { mockGetTodosHandler } from '~/apis/mocks/v2/todos/id/get.ts';
import { mockPostTodosHandler } from '~/apis/mocks/v2/todos/id/post.ts';

export const server = setupServer([mockGetTodosHandler, mockPostTodosHandler]);

これでMSWのセットができ、APIリクエストをモックすることが出来ます。

setupServerにセットする作業

このようにして、ほとんど作業なしでモックを作成できていましたが、APIを追加するたびに行う「MSWのハンドラーをsetupServer にセット」が作業になっている…と感じ、自動化したい!と思ったので、自動化してみました。

generateMockHandlers.jsでハンドラーの配列を自動生成

yarn hygen generators mocksで雛形を作成した時点でハンドラーは作成できているので、その後に、ハンドラーの配列を自動生成するようにしました。

これは大きく分けて、次の3つを行なっています。

  1. ハンドラーを全てimportする
  2. importしたハンドラーをmockHandlersという配列に格納する
  3. 上記1と2を持つファイルを作成する
// generateMockHandlers.js

const fs = require('fs');
const path = require('path');

// ハンドラーを定義しているpathを実行時に引数で受け取る(ex: ./src/apis/mocks)
const directoryPath = process.argv[2];

// 生成するファイルを実行時に引数で受け取る(ex: ./src/apis/Autogen/mockHandlers.ts)
const outputPath = process.argv[3];

// import文を生成するためにハンドラーを定義しているファイルのパスを取得
const getFilePathsInDirectory = (directoryPath) => {
  const entries = fs.readdirSync(directoryPath);
  const filePaths = [];

  for (const entry of entries) {
    const entryPath = path.join(directoryPath, entry);
    const stats = fs.statSync(entryPath);

    if (stats.isDirectory()) {
      // サブディレクトリの場合、再帰的にパスを取得
      const subDirectoryPaths = getFilePathsInDirectory(entryPath);
      filePaths.push(...subDirectoryPaths);
    } else {
      // ファイルの場合、パスを追加
      // import文に使用するので絶対パスへの変換や.tsなどを削除
      const transformedPath = `"${entryPath.replace('src', '@').replace('.ts', '')}"`;
      filePaths.push(transformedPath);
    }
  }

  return filePaths;
};

const filePaths = getFilePathsInDirectory(directoryPath);

// getFilePathsInDirectoryで取得したパスを使ってimport文を作成する
const generateImportPath = (filePaths) =>
  (importPaths = filePaths.map((path, i) => `import * as mock${i + 1} from ${path};`));

// mockHandlersとしてexportする配列を作成
const generateMockHandlers = (filePaths) => {
  return [...Array(generateImportPath(filePaths).length)].map(
    (_, i) => `...Object.values(mock${i + 1})`,
  );
};

fs.writeFile(
  outputPath,
  generateImportPath(filePaths).join(',').replaceAll(',', '').replaceAll(',', '') +
    '\n\n' +
    'export const mockHandlers =' +
    `[${generateMockHandlers(filePaths)}]`,
  (err) => {
    if (err) throw err;
  },
);

作成されるファイルは次のようになります。
全てのハンドラーがimportされ、mockHandlersという配列に全て格納されています。

// src/apis/Autogen/mockHandlers.ts 

import * as mock1 from '@/apis/mocks/posts/get';
import * as mock2 from '@/apis/mocks/posts/id/post';
import * as mock3 from '@/apis/mocks/todos/get';
import * as mock4 from '@/apis/mocks/todos/id/post';

export const mockHandlers = [
  ...Object.values(mock1),
  ...Object.values(mock2),
  ...Object.values(mock3),
  ...Object.values(mock4),
];

最終的には、このようにsetupServerに渡すことでモックの準備が整います。

import { setupServer } from 'msw/node';
import { mockHandlers } from '@/apis/Autogen/mockHandlers';

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

実際には、次のようにpackage.jsonのscriptに定義することで、 yarn mocks:new を実行すると自動でモックの作成からMSWのセットまで行えるようにしています。

"gen:mockHandlers": "node ./src/apis/generateMockHandlers.js ./src/apis/mocks ./src/apis/Autogen/mockHandlers.ts"
"mocks:new": "hygen generators mocks && yarn gen:mockHandlers"

おわりに

この自動化の前から、MSWのハンドラーの作成はhygenで雛形を作成することも出来ていて、作成するコストは低く、型安全で開発体験は良いものでした。
ただそれでも、APIを追加するたびに同じ実装をしていることに気がついたので、今回の自動化を試してみました。
この作業にかけた時間は1時間くらいだったので、将来の時間の節約になりました。また、自動化できる作業をしなくて済むことになり、気分も良かったです。
このように、小さなことでも作業になっていたら、自動化できないか??と一度考える癖はつけておこうと思った出来事だったので、ブログとして紹介させていただきました。

この記事のgenerateMockHandlers.js は実行時にハンドラーがあるディレクトリと生成先のファイルを指定することができ、他のプロジェクトにも適用できると思いますので、試してみていただけたら嬉しいです。

株式会社スタメン

Discussion