Open32

next testmode 素振り

mugimugi
mugimugi
mugimugi

元々の問題は↓のissue
https://github.com/mswjs/msw/issues/1644

Nodeでmswを動かす場合、httpやhttpsにパッチを当てて動かすが、Next.jsの場合はページごとのプロセスが都度生成されるため、パッチを引き継ぐことが出来なかったらしい

mugimugi
  • fixtureで立てたhttpサーバーはPlaywrightのworkerのプロセスに紐づいて生きてる
  • mswでパッチを適用した場合、Playwrightのworkerが生きてる限りは有効
  • Next側からのリクエストは↑を経由するため、Next側のプロセスの状態に関わらず、テスト上で設定したmswを経由してくれる

ということかもしれない

mugimugi

実際試してみる

mugimugi

READMEによると、mswのインストールは

Optionally install MSW in your project

とあり、そもそも任意らしい

mugimugi

fetch-mock を使う方法で試す

mugimugi

Page

export default async function Home() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  const json = await res.json();

  return (
    <main>
      <dl>
        <dt>id</dt>
        <dd>{json.id}</dd>
        <dt>title</dt>
        <dd>{json.title}</dd>
        <dt>body</dt>
        <dd>{json.body}</dd>
      </dl>
    </main>
  );
}
mugimugi

テスト

import { test, expect } from "next/experimental/testmode/playwright";
import fetchMock from "fetch-mock";

Object.assign(fetchMock.config, { Headers, Request, Response, fetch });

test.use({ nextOptions: { fetchLoopback: true } });

test("/", async ({ page, next }) => {
  fetchMock.get("https://jsonplaceholder.typicode.com/posts/1", {
    id: 123,
    title: "Test Title",
    body: "Test Body",
  });

  await page.goto("/");

  const body = page.locator("body");
  await expect(body).toHaveText(/123/);
  await expect(body).toHaveText(/Test Title/);
  await expect(body).toHaveText(/Test Body/);
});
mugimugi

パスした

モックのロジックを消すとちゃんと落ちる

mugimugi

MSW使わなくても、これで十分事足りるケースも多いかも?

mugimugi

Or use your favorite Fetch mocking library

に気をとられていたが、そもそも何も入れなくても引数の next 使えばモックできるぽい
https://github.com/vercel/next.js/tree/canary/packages/next/src/experimental/testmode/playwright#use-the-nextexperimentaltestmodeplaywright-to-create-tests

mugimugi

これでいけた

import { test, expect } from "next/experimental/testmode/playwright";

test("/", async ({ page, next }) => {
  next.onFetch((request) => {
    if (request.url === "https://jsonplaceholder.typicode.com/posts/1") {
      return new Response(
        JSON.stringify({
          id: 123,
          title: "Test Title",
          body: "Test Body",
        }),

        { headers: { "Content-Type": "application/json" } }
      );
    }

    return "abort";
  });

  await page.goto("/");

  const body = page.locator("body");
  await expect(body).toHaveText(/123/);
  await expect(body).toHaveText(/Test Title/);
  await expect(body).toHaveText(/Test Body/);
});
mugimugi
mugimugi

この handleFetch の段階で、this.fetchHandlerがあれば実行されてるのがわかる。
https://github.com/vercel/next.js/blob/87b66f64e53b9e5867cc88a7d648f4ab0af816ac/packages/next/src/experimental/testmode/playwright/next-fixture.ts#L33-L47

これは、テスト側で next.onFetch を実行した場合に渡したものが設定されてる
https://zenn.dev/mugi/scraps/dbcc69324a4856#comment-706b98d4a08fc3

というわけで、next.onFetchに渡したハンドラがモックとして動く

mugimugi

npm install -D msw して、次のテスト書けば終わり

import { test, expect, rest } from "next/experimental/testmode/playwright/msw";

test("/", async ({ page, msw }) => {
  msw.use(
    rest.get("https://jsonplaceholder.typicode.com/posts/1", (req, res, ctx) =>
      res.once(
        ctx.status(200),
        ctx.json({
          id: 123,
          title: "Test Title",
          body: "Test Body",
        })
      )
    )
  );

  await page.goto("/");

  const body = page.locator("body");
  await expect(body).toHaveText(/123/);
  await expect(body).toHaveText(/Test Title/);
  await expect(body).toHaveText(/Test Body/);
});
mugimugi

next/experimental/testmode/playwright/msw に定義の test は、↑で見た NextFixtureとNextWorkerFixtureを使ったCustomFixtureを継承している。

その上で、 next.onFetch ですべてのリクエストをインターセプトして msw に投げてる。

mugimugi

すべてをインターセプトするので、デフォルトのまま next/experimental/testmode/playwright/msw を使うと、msw向けのハンドラが存在しないAPIを呼び出した瞬間エラーになる