Back to posts

AstroサイトのE2EはPlaywrightの最小構成から

このサイトには、PlaywrightによるE2Eとスクリーンショット回帰を入れています。と言うと大げさですが、テストの本数は多くありません。個人ブログのE2Eは「全部を守るテスト」にした瞬間に保守が本体を食い始めるので、壊れると困る導線だけを決めて、そこから最小構成で始める方針にしました。

この記事では、導入の設定、最初に書くテスト3本の選び方、スクリーンショット回帰の割り切り方をまとめます。フレームワーク側の事情はAstro前提ですが、考え方自体は静的サイト全般に使えるはずです。

守る範囲を先に決める

最初にやるのは、テストコードを書くことではなく「何が壊れたら困るか」の棚卸しです。このサイトで挙がったのは次の3つでした。

対象なぜ守るかテストの形
トップページ顔になるページで、レイアウト崩れやビルド失敗に一番気づきにくい表示とタイトルの確認+スクリーンショット
記事ページ本文が出ない・タイトルがずれる事故はブログの本体に関わる記事を開いてタイトルと本文の見出しを確認
検索静的サイトでは数少ない「動く部品」で、壊れても見た目では分からないキーワードを入力して結果のリンクが出ることを確認

逆に、守らないものも決めました。全記事の巡回、細かい文言の一致、外部リンクの死活は入れていません。記事が増えるたびにテストの実行時間と修正の手間が増える構成は、個人運用では続きません。「増やすのは、実際に壊れて困ったものが現れてから」という順番にしています。

導入:サーバー起動まで設定に寄せる

導入手順は次のとおりです。

  1. プロジェクトのルートで npm init playwright@latest を実行し、テスト用のフォルダと設定ファイルを用意する
  2. npx playwright install でテスト用ブラウザを取得する
  3. playwright.config.ts に webServer と baseURL を書く(後述)
  4. テストを1本書いて npx playwright test で実行する

肝になるのは3番です。E2Eで面倒なのはテストの中身より「実行のたびにサーバーを手で起動すること」で、ここを設定に寄せると運用がだいぶ楽になります。

Astroの場合、テスト対象は開発サーバー(astro dev)ではなく、ビルド成果物を配信する astro preview にしています。astro preview は astro build で生成された dist/ をそのまま配信するので、本番に出る形に一番近い状態をテストできます。ただし preview 自体はビルドをしないので、コマンド側でビルドとセットにするのがポイントです。

// playwright.config.ts
import { defineConfig } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  use: {
    baseURL: "http://localhost:4321",
  },
  webServer: {
    command: "npm run build && npm run preview",
    url: "http://localhost:4321",
    reuseExistingServer: !process.env.CI,
    timeout: 120 * 1000,
  },
});

webServer にコマンドとURLを書いておくと、Playwrightがテスト前にサーバーを起動し、URLが応答を返すまで待ってからテストを始めます。reuseExistingServer: !process.env.CI は、ローカルで既にpreviewが立っていればそれを使い回す指定で、手元の作業を止めずにテストだけ回せます。ビルドを含む分、起動は少し重くなるので timeout はデフォルトの60秒から少し伸ばしています。baseURL を書いておけば、テスト側は page.goto("/") のような相対パスで書けます。

注意: astro preview はビルド済みの dist/ を配信するだけなので、テスト対象を最新の状態にしたい場合はビルドを必ず挟んでください。古い dist/ が残っていると、直したはずの表示を「まだ壊れている」と誤判定します。

最初のテスト3本

テストは、棚卸しした3対象に1本ずつ対応させます。書き方の原則は「ユーザーが見るものを、ユーザーの操作で確認する」ことです。内部の関数やDOMの細部には触りません。

import { test, expect } from "@playwright/test";

test("トップページが表示される", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveTitle(/PASTORA/);
});

test("記事ページでタイトルと本文が出る", async ({ page }) => {
  await page.goto("/");
  await page.getByRole("link").filter({ hasText: "記事タイトル" }).first().click();
  await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
  await expect(page.locator("article")).not.toBeEmpty();
});

test("検索で結果のリンクが出る", async ({ page }) => {
  await page.goto("/search/");
  await page.getByRole("textbox").fill("Astro");
  await expect(page.locator("#search").getByRole("link", { name: /Astro/ }).first()).toBeVisible();
});

細部はサイトの実装に合わせて読み替えてください。意識しているのは次の点です。

  • セレクタは getByRole など「画面上の意味」で取る。クラス名に寄せると、デザイン変更のたびにテストが壊れます
  • 記事ページのテストは特定の記事URLを直書きせず、一覧からたどる。記事の入れ替えでテストが死なないようにするためです
  • 検索のテストは「入力できること」ではなく「結果のリンクが返ること」まで見る。検索は表示が正常でも中身が空、という壊れ方をするためです(検索の組み込み自体は Pagefindで静的サイトに検索を足す手順と注意点 で扱いました)

スクリーンショット回帰は代表ページだけ

見た目の崩れはDOMのテストでは拾えないので、スクリーンショット比較も入れています。Playwright標準の toHaveScreenshot() を使う形です。

test("トップページの見た目", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveScreenshot("home.png", { fullPage: true });
});

運用のルールは絞っています。

  1. 対象はトップページなど代表ページだけにし、全ページには広げない
  2. 初回実行では基準画像が生成されてテストは失敗扱いになるので、生成された画像を目で見てからリポジトリにコミットする
  3. 意図したデザイン変更で差分が出たら、差分画像を確認したうえで --update-snapshots で基準画像を更新する

注意: スクリーンショットの描画結果は、OSやブラウザのバージョン、フォント環境で変わります。基準画像を作った環境と違う環境で実行すると、中身が同じでも差分が出ることがあります。基準画像は「作った環境のもの」として扱い、環境を変えたときは作り直しを前提にしてください。

差分が出たときの判断は、機械的に決めています。意図した変更なら基準を更新、意図していない変更なら本体のバグとして直す。あいまいなのは「フォントのレンダリング差」のような環境起因のもので、これは無理に基準へ吸収せず、対象ページを減らすか比較をやめる判断をします。回帰テストは、失敗したときに「また環境のせいか」と流されるようになったら終わりです。

CIに載せる前に、ローカルで回す習慣を先に作る

最初からCIに載せることはしませんでした。理由は単純で、ローカルで回らないテストはCIでも回らず、赤いCIはそのうち無視されるからです。

今の形は、記事を足したりレイアウトを触ったりしたときに、手元で npx playwright test を実行するだけです。webServer のおかげでサーバー起動の手順がなく、コマンド1つで完結します。まずはこの状態で「テストが実際に事故を拾うか」「実行が面倒になっていないか」を見て、困りごとが出たら、その解消としてCI化や対象の追加を検討する。テスト基盤の整備自体が目的にならないよう、増やす判断は常に後ろに置いています。

位置づけとしては、ローカルのテストは公開前の確認フローの手前の砦です。デプロイされたプレビューで最終確認をする流れは Cloudflare Pagesのプレビューデプロイ運用メモ にまとめました。

テストは書いた量ではなく、壊れたときに気づけるかどうかで価値が決まります。最小の3本と代表ページのスクリーンショットでも、「公開したサイトが真っ白だった」「検索が死んでいた」という一番避けたい事故はかなり拾えます。個人ブログのE2Eは、そのくらいの割り切りから始めるのがちょうどいいと思っています。

参考