こんにちは。小学生向けのニュースサイト、こどもニュースをつくっています。
このサイトは Next.js の静的書き出し(output: 'export')で、ビルドすると web/out に HTML が並びます。それを Cloudflare Pages に上げているだけです。
E2E は npm run dev に対して回すのが手軽ですが、本命のスイートはビルド成果物に対して回しています。理由と、そのための設定の話です。
dev と本番で挙動が違う class がある
dev サーバに対する E2E は速くて便利です。ただ、dev でしか出ない挙動と、本番でしか出ない挙動があります。
- dev には HMR のクライアントが入る。本番には無い
- dev は都度レンダリングする。本番は事前生成された HTML を返す
- 本番ビルドでは React の開発時警告が剥がれる
- 静的書き出しでは、動的なルートが実際に生成されているかどうかがビルド時に決まる
最後が特に効きます。ページを増やしたときに generateStaticParams の実装が間違っていると、dev では見えるのに web/out にファイルが無い、ということが起きます。dev に対する E2E は、これを一切検出できません。
デプロイするものと同じ成果物を検証したいので、webServer にビルドと配信をまとめて書いています。
webServer: {
command: `npx tsx scripts/build-web-cached.ts && npx serve web/out -l ${PORT} --no-clipboard --no-port-switching -L`,
env: { KODOMO_NEWS_SITE_URL: BASE_URL },
cwd: '../../',
}
serve で web/out を配信するだけです。Next.js のサーバは介在しません。本番の Cloudflare Pages も静的ファイルを返すだけなので、構成としてはほぼ同じになります。
環境変数でサイト URL を差し替える
静的サイトでは、canonical や sitemap に絶対 URL が焼き込まれます。本番の値のままビルドすると、テストで検証しづらいです。
// 本番 URL の既定 (web/.env.production の KODOMO_NEWS_SITE_URL) をテスト用に上書きし、
// spec の localhost:4501 前提(sitemap/robots/canonical の assert)を維持する。
env: { KODOMO_NEWS_SITE_URL: BASE_URL },
ビルド時に読む環境変数を、テスト用の値で上書きしています。これで sitemap の中身をそのまま検証できます。
ここは「テストのために本番と違うものをビルドしている」ことになるので、少し気持ち悪さがあります。ただ、URL 以外は同じなので、割り切りとしては許容範囲だと考えています。逆に本番 URL のままビルドすると、テストは URL を無視して検証することになり、そちらのほうが穴が大きいです。
ポートを固定しない
同時に複数の作業を進めているとき、ポートが衝突します。このプロジェクトでは git worktree を使うので、2 つの pre-commit が同時に走ることがあります。
// main=4501 / worktree=4502..4550(複数 worktree の pre-commit が衝突しない)
const PORT = WEB_E2E_PORT;
ポートは作業ディレクトリのパスから決定的に導いています。ランダムにすると、テストが失敗したときに「どのサーバを見ればいいか」が分からなくなります。決定的なら、同じ worktree では常に同じポートです。
ビルドが重いので、入力が同じなら省略する
この構成の弱点は、E2E のたびにビルドが走ることです。pre-commit で回すには重すぎます。
そこで、ビルドの入力(ソース・データ・ロックファイル・環境変数・Node のバージョン)のハッシュを取って、前回と同じなら省略するようにしました。
build-web-cached: 入力(web/ lib/ data/ 等)が前回ビルドから不変なら next build を省略。
KODOMO_NEWS_FORCE_BUILD=1 で無条件ビルド。
判定できないときは必ずビルドする側に倒しています。省略の判定が壊れたときに古い成果物を検証し続けるのが、いちばん困る失敗だからです。
並列数はコア数まで上げない
Playwright の worker 数は、既定でコア数の 50% にしています。100% にすると速くなりそうですが、実測では逆でした。
同じマシン(32 スレッド)でビルドをキャッシュ済みにして測ると、100% は 6 回中 2 回失敗、50% は 9 回中 0 回失敗です。所要時間はどちらも 12〜13 秒台で、差は誤差でした。
1 worker が Chromium 1 プロセスなので、コア 1 本には収まりません。worker をコア数に近づけると、描画や hydration の待ちが伸びて、アサーションのタイムアウトを踏みます。
速度が変わらないなら、安定する側を選ぶだけです。実験したいときのために、環境変数で上書きできるようにしてあります。
dev 用の設定も残す
とはいえ、開発中に毎回ビルドを待つのは無駄です。dev サーバに対して回す設定も別に用意しています。
- 本命(pre-commit・
npm test): ビルド成果物に対して - 日常の確認: 起動済みの dev サーバに対して
設定ファイルを分けて、共通部分だけを 1 か所にまとめています。速い確認と正しい確認は、どちらか一方に寄せずに両方持っておくのがよかったです。
まとめ
- 静的書き出しのサイトでは、dev サーバへの E2E は「生成されなかったページ」を検出できない
- 本命のスイートは
build && serve outに対して回す。デプロイするものと同じ成果物を検証する - 絶対 URL を焼き込む生成物があるなら、ビルド時の環境変数をテスト用に上書きする
- ポートは作業ディレクトリから決定的に導く。ランダムにしない
- ビルドが重いなら入力ハッシュで省略する。判定できないときはビルドする側に倒す
- worker 数はコア数まで上げない。速度が変わらないなら安定する側を選ぶ
- 速い確認用の設定も別に残す