msw で delay 使用時、Vitest においてテスト時の delay は 0 にする

msw の RequestHandler はローカル開発や storybook、テストで同じものが使用されることがありますが、テストでは delay を 0 にしたいことがあります。ここでは Response transformer を使ってテスト時にその delay を 0 にする方法を紹介しています。
2023.01.13

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

msw を使うとき、ローカルで開発するためレスポンスに delay を使用することがあります。

// src/mocks/utils/handlers.ts
export const handlers: RestHandler<MockedRequest<DefaultBodyType>>[] = [
  rest.get(client.v1.users.$path(), (_, res, ctx) => {
    return res(ctx.delay(), ctx.json(usersResponseDummy));
  }),
  rest.get(client.v1.date.$path(), (_, res, ctx) => {
    return res(ctx.delay(2000), ctx.json(dateResponseDummy));
  }),
];

このときに作成した RequestHandler、上記のサンプルでは GraphQL ではなく REST なので RestHandler ですが、こうしたハンドラを Storybook やテストで共有して使用する場合、delay があったとしてもおそらく Storybook ではとくに問題になりません。

ですがテストでは delay の分余計に時間がかかってしまうので、テスト時にはこのディレイを無効化したくなります。

Response transformer を使った方法

いくつか方法はありますが、ここでは Response transformer を使ってテスト時の delay を 0 にしてみます。

import { compose, context, ResponseTransformer, RestContext } from "msw";

export const delay = (
  duration?: Parameters<RestContext["delay"]>[0]
): ResponseTransformer => {
  if (import.meta.env.MODE === "test") {
    return compose(context.delay(0));
  }

  return compose(context.delay(duration));
};

今回は Vitest を使用しているので import.meta.env.MODEtest のとき、delay を 0 にしています。

これを使用する場合には次のように記述します。

import { delay } from "@/mocks/utils/delay";

export const handlers: RestHandler<MockedRequest<DefaultBodyType>>[] = [
  rest.get(client.v1.users.$path(), (_, res, ctx) => {
    return res(delay(), json(usersResponseDummy));
  }),
  rest.get(client.v1.date.$path(), (_, res, ctx) => {
    return res(delay(2000), ctx.json(dateResponseDummy));
  }),
];

作成した delay のテストを書く

mswdelay をモックして、実行時の引数が正しく渡せているかをテストします。

Vitest の Mock は 公式ドキュメントの Cheat Sheetがわかりやすいです。module をモックしたいので Modules の項目を参考に、delay をモックします。

vi.mock("msw", async () => {
  const actual = await vi.importActual<typeof import("msw")>("msw");
  return {
    ...actual,
    context: {
      ...actual.context,
      delay: vi.fn(),
    },
  };
});

ここでは delay のみをモックすればいいので、importActual を使っています。jest だと requireActual なので、私はそれに気づかずちょっとハマってました。

次にテストごとに環境変数を変更する方法ですが、こちらも先程の公式ドキュメントの Cheat Sheetに記載されています。

const originalViteEnvMode = import.meta.env.MODE;

beforeEach(() => {
  import.meta.env.MODE = originalViteEnvMode;
});

開発時とテスト時にそれぞれ delay の引数が正しく渡せているかを確認するテストをしてみます。

describe("delay", () => {
  it("開発時に delay が指定された時間になること", () => {
    import.meta.env.MODE = "development";
    const delayMock = (context as Mocked<typeof context>).delay;

    delay(100);

    expect(delayMock.mock.calls.length).toBe(1);
    expect(delayMock.mock.calls[0][0]).toBe(100);
  });

  it("テスト時に delay が 0 になること", () => {
    const delayMock = (context as Mocked<typeof context>).delay;

    delay(100);

    expect(delayMock.mock.calls.length).toBe(1);
    expect(delayMock.mock.calls[0][0]).toBe(0);
  });
});

その他の方法

参考にした discussions のリンク を見ていただけるとわかるのですが、別の方法としてはシンプルに ctx.delay() 内に数値を指定するとき、引数に関数でラップした数値を ctx.delay(ms(100)) として ms() がテストのときに 0 を戻すようにしてあげる方法があります。

参考