🧑‍⚕️

MSW v2 に移行するときの注意点

2024/05/21に公開

TL;DR

  • MSW v2 でも実行できたこと
    • Jest を使ったテスト
    • Storybook
    • Next.js の開発サーバでの実行
  • MSW v2 で実行できなかったこと
    • Next.js の Experimental test mode for Playwright を使ったテスト

はじめに

私が開発に関わっているプロダクトでは MSW v1 を利用していました。 MSW v2 がリリースされてしばらく経つので移行を試みましたが、諦めてしばらくは v1 を使い続けることにしました。
この記事ではそのように決定した理由や、その他の移行作業でやったことをまとめます。

環境

この記事では、以下のバージョン間の移行を試みた結果をまとめています。

- msw: 1.3.3 -> 2.2.14
- msw-storybook-addon: 1.10.0 -> 2.0.2
- next: 14.2.3
- jest: 29.7.0
- storybook: 8.0.6

Next.js の Experimental test mode for Playwright が MSW v2 に対応していない

Next.js の Experimental test mode for Playwright がまだ MSW v2 に対応していないようです。

https://github.com/vercel/next.js/issues/63338

対象のプロダクトでは、 Next.js の App Router を使ったアプリケーションの動作のテストや @axe-core/playwright を使った動作のテストに Experimental test mode と MSW を利用して実装しています。
これらのテストをそのまま動作するように、いったん MSW v2 への移行を諦めました。

MSW の v1 から v2 に移行する場合にも注意が必要ですが、すでに v2 を利用していて Experimental test mode を利用する場合には、 MSW ではないモックサーバやテスト環境のサーバを利用することで問題を回避する必要があります。

その他の移行作業のまとめ

上記の理由で v2 への移行を諦めましたが、その点を除くと問題なく v2 に移行できそうでした。
おまけとしてその他の移行作業を掲載します。

基本的にはこちらのガイドラインに沿って移行作業を進めています。

https://mswjs.io/docs/migrations/1.x-to-2.x/

主な作業は以下の通りです。

  • ハンドラの書き換え
  • Jest 用の設定を追加
  • msw-storybook-addon を v2 にアップグレードする

ハンドラを書き換える

対象のプロダクトでは GraphQL を使っているので、 graphql API における実装を例にしています。

v1 から v2 の移行では、次のようにハンドラを書き換える必要があります。
利用する機能が少し変わるだけで、基本的には愚直な書き換えだけなのでそれほど難しくないです。

import { GetUserDocument } from "./generated/types"; // GraphQL Code Generator で生成された Document Node オブジェクト

// v1 だとこんな感じ
graphql.query(GetUserDocument, (req, res, ctx) => {
  return res(
    ctx.data({
      __typename: "Query",
      user: {
        __typename: "User",
        id: "user:0001",
        name: 'John',
      }
    })
  );
});

// v2 だとこんな感じ
graphql.query(GetUserDocument, ({ query, variables }) => {
  return HttpResponse.json({
    data: {
      __typename: "Query",
      user: {
        __typename: "User",
        id: "user:0001",
        name: 'John',
      },
    },
  });
});
ts-morph を使ってハンドラの書き換えを自動化する

前述の通り書き換え自体は簡単ですが、量が多いと大変な作業になります。数百のハンドラを人手で修正しようとすると途方もない時間がかかります。

そこで ts-morph というライブラリを使って機械的な書き換えをやってみました。

ライブラリ自体の詳しい説明は省略してますが、先ほど出した例はこのようなスクリプトで書き換えられます(tsx を使って npx tsx transform.ts で実行できます)。
実際にはこれだけで全てのケースに対応できるわけではないので適宜細かい修正を加えながらハンドラを書き換えました。

transform.ts
import { Project, ts, type SourceFile } from 'ts-morph';

function replace(file: SourceFile) {
  file.transform((traversal) => {
    const node = traversal.visitChildren();

    if (ts.isArrowFunction(node)) {
      if (node?.parent?.getFirstToken()?.getText() === 'graphql') {
        const functionBody = node.body.getChildAt(1);

        const statement = functionBody.getChildren().filter(ts.isStatement)[0];

        if (statement && ts.isReturnStatement(statement)) {
          const target = statement.getChildAt(1);

          // res(...) の中身を取り出す処理
          if (ts.isCallExpression(target) && target.expression.getText() === 'res' && target.arguments[0]) {
            const ctxData = target.arguments[0]

            // ctx.data(...) の中身を取り出す処理
            if (ts.isCallExpression(ctxData) && ctxData.expression.getText() === 'ctx.data' && ctxData.arguments[0]) {
              const dataObject = ctxData.arguments[0];
              if (ts.isObjectLiteralExpression(dataObject)) {

                // 取り出した中身を使って新しい API に置き換える処理
                return traversal.factory.updateArrowFunction(
                  node, [], [],
                  [
                    ts.factory.createParameterDeclaration(
                      undefined,
                      undefined,
                      '{ query, variables }',
                    ),
                  ],
                  undefined,
                  node.equalsGreaterThanToken,

                  // ctx.data(...) の中身を HttpResponse.json({ data: ... }) の形式に詰め替える処理
                  ts.factory.createBlock([
                    ts.factory.createReturnStatement(
                      ts.factory.createCallExpression(
                        ts.factory.createPropertyAccessExpression(
                          ts.factory.createIdentifier('HttpResponse'), ts.factory.createIdentifier('json'),
                        ),
                        undefined,
                        [
                          ts.factory.createObjectLiteralExpression(
                            [
                              ts.factory.createPropertyAssignment(
                                ts.factory.createIdentifier('data'),
                                ts.factory.createObjectLiteralExpression(dataObject.properties, true),
                              ),
                            ],
                          ),
                        ],
                      ),
                    ),
                  ]),
                );
              }
            }
          }
        }
      }
    }

    return node;
  });

  file.save();
}

function main() {
  const project = new Project({
    tsConfigFilePath: `./tsconfig.json`,
  });

  project.getSourceFiles(['src/components/**/*.test.tsx']).forEach(replace);
}

main();

Jest の設定

対象プロジェクトで Jest を利用している場合、 ReferenceError: TextEncoder is not defined のようなエラーが出るようになります。
ガイドラインの Frequent Issues にあるように、ポリフィルを自作することで回避できるようになります。

https://mswjs.io/docs/migrations/1.x-to-2.x#requestresponsetextencoder-is-not-defined-jest

ただしポリフィルに含まれる undici のバージョンに注意が必要で、最新版の v6 を利用するとネットワークエラーが発生するようになります。以下の Discussion にあるように v5 を使うようと解決しました。

https://github.com/mswjs/msw/discussions/1915#discussioncomment-8069064

Storybook の設定

msw-storybook-addon の v2 が MSW v2 に対応したのでアップグレードします。
v1 では .storybook/preview.ts で mswDecorator を呼び出していましたが、 mswLoader を使うように変わっている点に注意が必要です。

.storybook/preview.ts
import {
-  mswDecorator
+  mswLoader
} from 'msw-storybook-addon';

const preview: Preview = {
-  decorators: [mswDecorator],
+  loaders: [mswLoader],
};

export default preview;

おわりに

すべての機能が移行可能ではなかったので、現時点では v2 への移行は見送りました。
ただし、 Experimental test mode での問題が解消すると移行できそうなことがわかったので、今後の更新を待って対応する予定です。

Discussion