📝

PoCプロジェクトでのMock Service Worker活用の変遷

2023/05/08に公開

ハコベル株式会社でフロントエンドエンジニアをしている大川です。

直近自分が携わったプロジェクトではUI実装にMock Service Worker(以下MSW)を活用してモックデータを用意してきました。
そのモックデータをもとにプロトタイプ実装やバックエンド開発と並行したUI開発を進めてきたので、活用方法を紹介します。

  • PoCのためにプロトタイプを実装している
  • フロントエンドとバックエンドで役割が分かれたチームでの開発方法を模索している

といった方のお役に立てれば幸いです。

プロジェクトでの利用技術

このあと記事で紹介する技術を以下に書き出しておきます。

フェーズ1: オペレーション理解のためのプロトタイピングと@mswjs/dataの活用

プロジェクト初期はユーザーインタビューなどを通してユーザーの業務オペレーションを理解し、プロダクトの全体設計をしました。
このとき「モックデータを基にUI実装を先行してプロトタイプを作り、ユーザビリティテストのような検証を通して具体的なフィードバックを得られないか」といった仮説をもっていました。

もともと本プロジェクトでは開発メンバー全員でGraphQL スキーマ定義の認識すり合わせ後にフロントエンド・バックエンド開発を並行させるスキーマ駆動開発を取り入れるためにMSWを利用していました。
しかし固定のモックデータではオペレーションの一部しか再現できなかったため、@mswjs/data を導入して機能全体が動作してみえるように改善しました。

@mswjs/data はデータモデルやリレーションをフロントエンドで定義し、ユーザー操作後のUI変化をモックデータで再現できるようにするものです。
データモデルの定義方法・利用方法の詳細は割愛しますが、モックデータを返すGraphQL のハンドラーは以下のように従来のAPIレスポンスを定義するコードの手前でモックデータを更新するようになりました。

// GraphQLハンドラーの例
export const mockSelectItems = graphql.mutation(
  SelectItemsDocument,
  (req, res, ctx) => {
    const { itemIds } = req.variables;

    // モックデータを更新(dbオブジェクトにデータモデルが格納されています)
    db.item.updateMany({
      where: {
        id: {
          in: itemIds,
        },
      },
      data: {
        selected: true,
      },
    });

    // Mutationのレスポンスを定義
    return res(
      ctx.data({
        __typename: "Mutation",
        selectItems: true,
      })
    );
  }
);

インタビューに必要なロジックを中心にモックデータを準備し、インタビュイーの方に実際にUIを操作してもらいながらフィードバックをもらう仕組みをUI実装メインで素早く構築できました。

フェーズ2: UIの最適化とGraphQL Code Generatorのプラグイン追加

業務オペレーション理解が深まりプロダクトの全体設計もできあがってきたので、次フェーズとしてプロダクトを普段の業務オペレーションに組み込んでもらうための改善をしていきました。
このフェーズでより早いサイクルでの改善を進めていくうえで、モックデータのメンテナンスが手間になっている課題がありました。

@mswjs/dataで構築したデータモデルとGraphQLスキーマから生成された型情報を連携できておらず、
GraphQLスキーマの変更に追従しにくい・対応漏れにも気づきにくい状態でした。

そもそもフェーズが変わって@mswjs/dataで作成したモックデータを活用する場面が減ったにも関わらず
GraphQLスキーマ変更に追従するためメンテナンス工数が発生していたのも改善点でした。

そこで、@mswjs/dataで用意したデータモデルの代わりに、GraphQL Code Generatorで以下のプラグインを追加してモックデータの作成関数を自動生成してもらうことにしました。

  • @graphql-codegen/typescript-msw : v1.1.6
    • GraphQLスキーマで定義されたクエリとミューテーションに対してMSW用のハンドラーを生成してくれるプラグイン
  • graphql-codegen-typescript-mock-data : v3.4.1
    • GraphQLスキーマをもとに、Fakerなどを利用したモックデータ作成関数を生成してくれるプラグイン

これらのプラグインを利用すると、MSWのハンドラー定義は以下のようになります。

// NOTE: src/libs/gql/generated-mock-data.tsは
// graphql-codegen-typescript-mock-dataの生成物
import {
  fakeItems,
} from "@/libs/gql/generated-mock-data";

// NOTE: src/libs/gql/generated-mock-handler.tsは
//  @graphql-codegen/typescript-mswの生成物
import {
  mockFetchItemsQuery,
} from "@/libs/gql/generated-mock-handler";

export const mockFetchItems = mockFetchItemsQuery(
  (req, res, ctx) => {
    return res(
      ctx.data({
        __typename: "Query",
        items: [
          fakeItem(),
          // NOTE: Fakerを利用したランダム値の代わりに固定値で上書き定義できる
          fakeItem({
            id: 1,
            name: 'item-name',
          }),
        ],
      })
    );
  }
);

上のコードではハンドラー定義とモックデータ作成にGraphQLスキーマから生成した関数を利用しています。
それにより、スキーマ変更やクエリのリクエスト・レスポンス内容が変更された場合に、再生成したコードと既存のハンドラー実装で型エラーが発生し対応漏れを防ぐことができます。
また、フィールドが追加された場合などはコード再生成によりfakeItem関数内部のみの変更となるためメンテナンスの工数を削減できるようになりました。

その他のTips

カスタムのスカラー値用にFakerの利用方法を変更する

graphql-codegen-typescript-mock-dataプラグインは設定ファイル内で、カスタムスカラ型の値をどう生成するか定義できます。
また、そのときにFakerの関数が指定できるだけではなく関数呼び出しのコードを文字列として登録できます。
以下の設定では、Timeというカスタムスカラ型に対して、ISO8601形式の文字列を返すように toISOString 関数を呼び出すところまで指定しています。

// codegen.tsからの一部抜粋
    "./src/libs/gql/generated-mock-data.ts": {
      plugins: [
        {
          "typescript-mock-data": {
            add: "import { faker } from '@faker-js/faker';",
            typesFile: "./graphql.ts",
            generateLibrary: "faker",
            prefix: "fake",
            addTypename: true,
            dynamicValues: true,
            terminateCircularRelationships: true,
            scalars: {
              Time: "faker.date.past().toISOString()",  // ISO8601形式の文字列を生成してもらう設定
              Upload: "system.fileName",
            },
          },
        },
      ],
    },

モックデータの内容をランタイムに切り替える(Service Worker専用)

Service WorkerでMSWが動作する場合にはLocationオブジェクトにアクセス可能です。
専用のクエリパラメータを用意して既存実装に影響なくランタイムで正常系とエラー系のレスポンスを切り替えることができます。

import {
  mockCreateItemMutation,
} from "@/libs/gql/generated-mock-handler";

// 開いているページのクエリパラメータにerror-handler=QueryNameが含まれているかチェックする
export const isErrorMode = (operationName: string): boolean => {
  // NOTE: serviceWorker内ではlocationオブジェクトが使える
  // https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/location
  if (typeof location === "undefined") return false;
  const searchParams = new URLSearchParams(location.search);
  return searchParams.getAll("error-handler").includes(operationName);
};

// NOTE: ローカル環境のURLで error-handler=CreateItemが指定されているとエラーを返す
export const mockCreateItem = mockCreateItemMutation(
  async (req, res, ctx) => {
    const errorMode = isErrorMode("CreateItem");
    return res(
      ctx.data({
        __typename: "Mutation",
        createItem: !errorMode
          ? {
              // 正常系レスポンス
              __typename: "CreateItemSuccessResponse",
              result: true,
            }
          : {
              // エラーレスポンス
              __typename: "CreateItemArgumentErrorResponse",
              errors: ['item nameは100文字以内で入力してください'],
            }
       })
    )
  }
)

さいごに

この記事では、プロジェクトの異なるフェーズ毎のMSW利用方法とTipsを紹介しました。

もともとスキーマ駆動開発とGraphQL Code GeneratorによってAPI呼び出しロジック実装の工数削減はできていました。
そこにMSWを組み合わせることでプロトタイプの素早い構築、バックエンド開発との並列化などが実現できてとても便利です。
今後はこのモックデータをUIテストにも利用して自動テストを整備していきたいです。

Hacobell Developers Blog

Discussion