next testmode 素振り
ためしてみる
該当のPR
コードやREADMEはこのあたり
を見ると、next dev 起動時に --experimental-test-proxy を付与するのがポイントぽい
The "fetch loopback" mode can be configured in the playwright.config.ts or via test.use() with a test module. This option loops fetch() calls via the fetch() of the current test's worker.
fetchLoopback
を使うと、現在のテストのworker上の fetch
を使うようにできるぽい
test
関数は、Playwrightの Custom Fixture
元々の問題は↓のissue
Nodeでmswを動かす場合、httpやhttpsにパッチを当てて動かすが、Next.jsの場合はページごとのプロセスが都度生成されるため、パッチを引き継ぐことが出来なかったらしい
Playwrightのworkerの単位で http
モジュール経由でhttpサーバーを立ててる
リクエストをwrapして、↑で立てたhttpサーバーのportに対して流すようにしてるぽい
- fixtureで立てたhttpサーバーはPlaywrightのworkerのプロセスに紐づいて生きてる
- mswでパッチを適用した場合、Playwrightのworkerが生きてる限りは有効
- Next側からのリクエストは↑を経由するため、Next側のプロセスの状態に関わらず、テスト上で設定したmswを経由してくれる
ということかもしれない
fetch-mock
を使う方法で試す
npm install -D fetch-mock
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>
);
}
テスト
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/);
});
パスした
モックのロジックを消すとちゃんと落ちる
MSW使わなくても、これで十分事足りるケースも多いかも?
Or use your favorite Fetch mocking library
に気をとられていたが、そもそも何も入れなくても引数の next
使えばモックできるぽい
これでいけた
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/);
});
この next
って何
このへんで定義してる
onFetch の実体は、NextFixtureのものが呼ばれる
実際の動作時には、まずfetchリクエストがproxy によって拾われる
↑での onFetch は、NextWorkerFixtureImpl のインスタンスにbindされてる
結果として、 this.proxyFetchMap
に登録されてるハンドラが実行される
this.proxyFetchMap
には、NextWorkerFixtureImplのonFetchを呼ぶと登録される
NextWorkerFixtureImplのonFetch は、NextFixture のインスタンス生成時に呼ばれてる。
handlerとして渡されてるのは、NextFixtureImplのhandleFetch
この handleFetch の段階で、this.fetchHandlerがあれば実行されてるのがわかる。
これは、テスト側で next.onFetch を実行した場合に渡したものが設定されてる
というわけで、next.onFetchに渡したハンドラがモックとして動く
msw
使ったのも試す
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/);
});
next/experimental/testmode/playwright/msw
に定義の test は、↑で見た NextFixtureとNextWorkerFixtureを使ったCustomFixtureを継承している。
その上で、 next.onFetch
ですべてのリクエストをインターセプトして msw に投げてる。
すべてをインターセプトするので、デフォルトのまま next/experimental/testmode/playwright/msw
を使うと、msw向けのハンドラが存在しないAPIを呼び出した瞬間エラーになる
自分で使うときとかは onUnhandledRequest
の設定によって、モック化されていないAPIはスルーさせたりもできるが、現状これは error
で固定されてる