株式会社Berry
🐝

E2Eテストをユニットテスト並みの実行時間に — Playwright並列化とGitHub Actionsチューニングの実践

に公開

私たちのチームでは、品質管理システム(QMS)SaaS「QMSmart」の開発で、約 100 spec / 140 テストケースの E2E テストを、PR ごとに CI で自動実行し、wall-clock 6〜8 分で完走させる運用が回っています。これはユニットテスト(Vitest)の CI とほぼ同じ所要時間です。

本記事では、これを実現するために行った

  1. 並列実行を前提にした E2E テストの構造設計
  2. Playwright workers による並列化
  3. GitHub Actions Runner の設定とチューニング(Kong 502 の根治まで)
  4. flaky テストを作らないための「Iron Law」
  5. E2E の開発・運用を支える AI エージェントスキル群

を紹介します。


CI実測のwall-clock time. 詳細は4章4. GitHub Actions: Runner 設定とチューニング

対象システムとスタック

  • フロントエンド: Vue 3 + TypeScript
  • バックエンド: Supabase(PostgreSQL / Auth / Storage / Edge Functions)
  • E2E: Playwright
  • CI: GitHub Actions(8core Larger Runner)

QMS ドメインの特性上、文書のレビュー・承認・電子署名・監査ログといった多ロール・多状態遷移のワークフローが主な検証対象です。承認時には PDF 生成 + S3 アップロードを行う Edge Function(1 呼び出し約 60 秒)が絡む、E2E 泣かせの処理もあります。

No 操作手順(S) No 期待結果(E)
S1 エディターロールで対象文書の版詳細ページを開く -
S2 「レビュー依頼」ボタンを押し、レビュアーを 1 名選択する
S3 「依頼する」ボタンを押す E1 文書ステータスが「ドラフト」から「レビュー依頼中」に変わる
E2 指定したレビュアーのタスク一覧に当該文書が追加される
E3 レビュー依頼の操作が監査ログに記録される

E2Eテストでテストしたい項目は、アプリケーション操作(S項目)と合格判定基準項目(E項目)で構成される

1. 従来型 E2E テストが抱える問題

E2E テストの価値は誰もが認める一方で、「遅い・壊れやすい・維持できない」という問題は業界全体で長く指摘されてきました。

  • 実行時間が長く、開発フローに載らない: Google Testing Blog の Just Say No to More End-to-End Tests(2015)は、E2E 偏重の戦略では実行時間と flake が膨張し、フィードバックが遅く失敗時の原因特定も難しいと指摘し、ユニット 70 / インテグレーション 20 / E2E 10 のテストピラミッドを推奨しました。
  • flaky(不安定): Google は自社基盤で「全テスト実行の約 1.5% が flaky な結果を報告し、テスト全体の約 16% が何らかの flakiness を持つ」(Flaky Tests at Google and How We Mitigate Them, 2016)、さらに「テストが大きい(E2E 的である)ほど flaky になる」(Where do our flaky tests come from?, 2017)と報告しています。Microsoft の研究(ISSTA 2019)でも社内 5 プロジェクトのテストケースの 4.6% が flaky で、flaky の再実行だけにテスト予算の数%〜十数%が消えるとされます。
  • メンテナンスサイクルが回らない: The Practical Test Pyramid(martinfowler.com)は E2E を「悪名高いほど flaky で、失敗の多くが偽陽性」と評します。「失敗しても誰も見ない → 直らない → さらに信頼を失う」という悪循環で、E2E・手動テストに偏ってピラミッドが逆転した「テストのアイスクリームコーン」アンチパターンに陥りがちです。

近年は Playwright の auto-waiting / web-first assertions、ブラウザコンテキストによるテスト分離並列実行・sharding と、道具立ての側は大きく進歩しました。ただし道具だけでは解決しません。テストの構造がデータ独立になっていなければ並列化できず、待機の規律がなければ auto-waiting をすり抜ける race が残るからです。

私たちも例外ではありませんでした。改善前の旧 E2E スイートはこうでした。

項目 旧 E2E(改善前)
実行契機 平日 JST 7:00のみ。PR では実行されない
実行時間 30〜56 分(timeout 150 分設定)
直近の結果 failureに気づいたエンジニアが直す

「PR で回らないので開発者は結果を見ない」「落ちていても機能開発の影響なのか分からない」「直すサイクルが回らない」——上に挙げた一般的問題を、そのままなぞっていました。

そこで既存スイートの改修ではなく、並列実行を前提にした新アーキテクチャをゼロから設計する判断をしました。

2. 並列化可能な E2E テストの構造設計

E2E の並列化で最初にぶつかる壁は Playwright の設定ではなく、テスト同士がデータと状態を共有していることです。逆に言えば、構造さえ正しければ並列化は「設定を変えるだけ」になります。

2.1 per-test fixture でデータ独立

すべてのテストは、自分専用のデータを Factory 経由で Supabase に直接 INSERT して作り、テスト終了時に teardown が派生レコード(レビュー・承認・署名)から Storage のファイルまで回収します。

// fixture 内部の bundle パターン(概念コード)
_factoryBundle: [
  async ({}, use) => {
    const supabase = createAdminClient();
    const bundle = createDocumentFactory(supabase);
    await use(bundle);
    await bundle.teardown(); // 派生レコード + Storage まで一括回収
  },
  { scope: "test" }
]

ポイントは 2 つあります。

  • shared state を一切持たない: 「テスト A が作った文書をテスト B が使う」を構造的に禁止。どのテストがどの worker でどの順に走っても互いに干渉しません。
  • UI でデータ準備をしない: UI 操作は「検証対象の操作」だけに使い、前提データは全て DB 直 INSERT。これは高速化(UI 経由のデータ準備は遅い)と安定化(前提部分の flaky 要因を排除)の両方に効きます。
  • UI 操作で生まれたデータも teardown に登録: アップロードなど UI 経由で生成されたエンティティは trackDocument(id) のような track 関数で明示的に回収対象へ登録します。

2.2 storageState でログインを省略

UI ログインは 1 回あたり数秒かかるうえ、認証サーバーという共有リソースへの負荷になります。globalSetup で全テストユーザー(4 ロール × 各 5 アカウント)のセッションを事前生成して storageState に保存し、各テストは Cookie 読み込みだけでロールを切り替えます。

test.use({ storageState: storageStatePath("admin") });

ロールごとにアカウントを 5 つずつ用意しているのは、並列 worker が同一アカウントを取り合って生じる干渉(同一ユーザーのタスク一覧が別テストのデータで揺れる等)を避けるためです。

2.3 レイヤー構造と POM(Page Object Model)

新規のE2Eスイートは、下向きの依存だけを許すレイヤー構造です。

spec → fixture → pages(POM) / helpers → repositories / precheckers / schemas → selectors / types
  • spec から page.locator(...) を書くことを禁止。Locator と操作・待機ロジックは Page Object[1]に閉じ込め、spec は「S1: レビュー依頼する」「E1: ステータスが変わる」というシナリオ記述に徹します
  • data-testid 文字列は selectors/ の定数に集約し、Page Object からのみ import
  • DB 検証は repositories/(SELECT のみ)、事前データ検証は precheckers/ + zod schema

この分離は保守性のためだけではありません。待機戦略(後述の Iron Law)を Page Object に閉じ込めることで、「flaky 対策が個々の spec 作者の力量に依存しない」状態を作るのが狙いです[2]

2.4 3 軸の Project 分割: load × release × feature

Playwright の project を {load}-{release}-{feature}(例: light-r140-document)という 3 軸で分割しています。

表現方法 役割
load light / heavy @heavy タグ 負荷プロファイル別に並列度を変える
release r140 など ディレクトリ リリースバージョン別のテスト資産管理
feature document など ディレクトリ 機能ドメイン別に CI ジョブを分ける

heavy は「承認時 Edge Function(PDF 生成 + S3、約 60 秒)を呼ぶテスト」で、それ以外は全部 light。この分割が次章の並列度チューニングの土台になります。

3. Playwright workers による並列化

構造がデータ独立なので、並列化は config で workers を上げるだけ——ではあるのですが、負荷プロファイル別に並列度を変えるのが実運用のキモでした。

// playwright.config.ts(抜粋・簡略化)
function lightProject(release, feature, testMatch, options?) {
  return {
    name: `light-${release}-${feature}`,
    grepInvert: /@heavy/,
    workers: process.env.CI ? (options?.ciWorkers ?? 5) : 4,
    fullyParallel: true,
    timeout: 60_000
  };
}

function heavyProject(release, feature, testMatch, options?) {
  return {
    name: `heavy-${release}-${feature}`,
    grep: /@heavy/,
    workers: 3,
    fullyParallel: false,
    timeout: 180_000
  };
}
  • light: UI 表示確認・単発 CRUD・ロール切替フロー。fullyParallel: true + workers 5(CI)
  • heavy: Edge Function の約 60 秒はほぼ I/O 待ちで CPU をほとんど使わないため、3並列で「待ち時間を重ねる」ことで総時間を圧縮。timeout は 180 秒に延長
  • 例外調整: 文書管理ドメインは CI で CPU 競合起因の flaky が出やすいことが計測で分かったため、project 単位のオプションで light=3 / heavy=2 に下げています

最後の点は重要な教訓でした。並列度は「上げられるだけ上げる」ものではなく、計測に基づいて project ごとに調整するものです。そのための計測基盤は次章で説明します。

4. GitHub Actions: Runner 設定とチューニング

4.1 全体構成: matrix × 8core Runner

CI は {load}-{release}-{feature} の project 単位で matrix 分割し、10 ジョブがそれぞれ独立した 8core Runner(ubuntu-24.04-8core)上で並列に走ります。各 Runner の中でさらに Playwright workers が並列実行する、二段の並列化です。

各ジョブは毎回まっさらな環境に

  1. supabase start(PostgreSQL / Auth / Storage / Kong / PostgREST をコンテナ起動)
  2. Edge Functions serve 起動
  3. vite build + preview サーバー起動(CI は dev サーバーではなくプロダクションビルドを検証)
  4. Playwright 実行

という「本物のフルスタック」を構築します。モックサーバーではなく実スタックなので、RLS(Row Level Security)やトリガ、Edge Function まで含めたリグレッションを検知できます。

実測の wall-clock はこうなりました。

ワークフロー 所要時間(成功ラン実測)
E2Eテスト全量(10 project 並列) 約 6〜8 分
E2EテストImpact(影響 spec のみ・後述) 約 4〜7 分
Vitest(components) 約 3 分
Vitest(repositories) 約 7〜8 分
ESLint 約 9 分
(参考)旧 E2E 30〜56 分

E2E がユニットテストや lint と同じ土俵の所要時間に収まっているため、「E2E だけ結果を待たずにマージする」という運用崩壊が起きません。

4.2 地味に効くセットアップの工夫

  • Supabase CLI のバージョンをピン留め: matrix で 10 ジョブが同時に latest を解決すると GitHub Releases API の rate limit に当たって落ちるため、明示バージョン指定
  • Playwright ブラウザのキャッシュ: ~/.cache/ms-playwright をバージョンキーでキャッシュ
  • CJK フォントのインストール: fonts-noto-cjk 等を入れないと、スクリーンショット証跡の日本語が豆腐になります
  • concurrency で同一 ref の実行をキャンセル: push が連続したとき古いランを自動キャンセル

4.3 並列化が暴いたインフラの限界(1): PostgREST プール枯渇

並列 worker が REST API を叩くと、PostgREST のデフォルト DB プール(10)が枯渇し、504 / タイムアウトが出始めました。Supabase CLI の config.toml には設定項目がないため、supabase start 直後に rest コンテナを環境変数差し替えで再作成して PGRST_DB_POOL=30 を注入するステップを挟んでいます。

4.4 並列化が暴いたインフラの限界(2): Kong の即時 502 ——keep-alive 再利用レース

並列化チューニングで最初わからなかったことが、数ミリ秒で返ってくる謎の 502 でした。テストデータ投入の POST /rest/v1/... が、まれに 8ms で 502 になる。PostgREST は終始健全。Kong(API Gateway)のログには recv() failed (104: Connection reset by peer) while reading response header from upstream が残る。

真因はHTTP keep-alive 接続の再利用レースでした。

  • Kong は PostgREST への上流接続を keep-alive でプールし再利用する(upstream_keepalive_idle_timeout 既定 60 秒)
  • 一方 PostgREST(Haskell Warp)はアイドル接続を約 30 秒で自分から閉じる
  • つまり 「サーバー側はもう閉じたのに、Kong はまだ生きていると思ってプールに保持している」30〜60 秒の窓が恒常的に存在する
  • その窓で Kong が死んだソケットに新規リクエストを書き込むと、カーネルが RST を返し、即時 502 になる
  • POST/PATCH は非冪等なので Kong は再試行せず、テスト失敗として表面化する

これは Kong 固有ではなく、nginx / Envoy / ALB でも知られた一般原理です(「接続を再利用する側の idle timeout は、サーバー側より必ず短くする」)。私たちは 2 段階で対処しました。

  1. Fix 1: KONG_UPSTREAM_KEEPALIVE_IDLE_TIMEOUT=10 で Kong 側を Warp より短くし、「閉じる主導権」を再利用する側に移す → アイドル起因の variant は根治
  2. Fix 2: それでも高負荷バースト中(125〜160 req/s)に、アイドルを介さず上流が閉じる variant が残ったため、KONG_UPSTREAM_KEEPALIVE_MAX_REQUESTS=1 で接続再利用そのものを無効化 → 「死んだ接続を掴む」経路が原理的に消滅

「リトライで吸収する」案は採りませんでした。非冪等リクエストのリトライは重複データのリスク管理が必要になるためで、再利用を無効化してレース面そのものを消す方が、CI の安定化としては構造的に安全という判断です(毎リクエスト TCP handshake のコストは、CI では安定性が性能に勝ります)。

こちらも config.toml に設定口がないため、CI 上で Kong コンテナを環境変数差し替えで再作成するスクリプトで適用しています。

4.5 「勘で並列度を触らない」ための計装

チューニングの試行錯誤を支えるため、CI ジョブに計測を常設しています。ホスト側の 3 つのモニタはいずれも /proc 直読みの bash スクリプトで、追加パッケージのインストールなしに Runner へ載せられます。

計装 使っている Linux の機能 見えるもの
CPU 競合モニタ PSI/proc/pressure/cpu, kernel 4.20+)+ /proc/statprocs_running / ctxt)+ /proc/loadavg worker 数が CPU に対して過剰かを SEVERE / WARN / NOTICE / HEALTHY の 4 段階で判定
リソースモニタ /proc/stat の CPU tick 差分、メモリ・ディスク・ネットワークの /proc カウンタ CPU(user/system/iowait)・メモリ・ディスク・ネットの使用量時系列と上位プロセス
バックエンド I/O 競合モニタ /proc/<pid>/fd のソケット inode + /proc/<pid>/net/tcpnetwork namespace 単位で読む) Edge Function の外部 I/O 詰まり(外部向け ESTABLISHED 接続の同時数と滞留秒数)
HTTP 5xx ログ + モード分類 (テスト側計装: fetch のラップ) 5xx を「keep-alive 再利用レース / edge isolate 死 / プール待ち」等に自動分類して Job Summary に表示
REST リクエストタイムライン (同上) req/s の時系列に 502 発生位置をピン表示(バーストとの相関が見える)
バックエンドレイテンシ集計 (同上) endpoint 別の p50 / p95 / max(処理時間の支配項を特定)

指標の選び方にはそれぞれ理由があります。

  • CPU 競合は PSI(Pressure Stall Information)[3]を主指標にする。CPU 使用率 100% は「よく働いている」と「足りない」を区別できませんが、PSI はタスクが CPU 待ちで止まっていた時間の割合を直接測ります。特に some(どれかのタスクが待った = 混雑度)は WARN の補助指標に、full(全 runnable タスクが同時に待った = 枯渇度)は SEVERE の主指標に、と使い分けています。run queue 長(/proc/statprocs_running)÷ nproc は「並列度を出し過ぎている」ことの観測値として併用します。
  • 外部 API 待ちは CPU 系指標では見えない。Edge Function が S3 や外部 API を待っている間、プロセスは socket 待ちで sleep しており PSI にも run queue にも現れません。そこで edge-runtime プロセスの /proc/<pid>/fd からソケット inode を集め、TCP 接続の状態と対応付けて「外部向け接続が何本・何秒滞留しているか」を直接数えます。このときコンテナ内プロセスのソケットはホストの /proc/net/tcp には載らないため、/proc/<pid>/ns/net で network namespace を特定し、namespace ごとに /proc/<pid>/net/tcp を読む必要がある——というのがコンテナ時代ならではの落とし穴でした。

「E2E が落ちた」と「CPU 競合が SEVERE だった」を突き合わせて、テストのバグなのか並列度の出し過ぎなのかを 1 ランで切り分けられるようにしてあります。前述の「document ドメインだけ workers を下げる」判断も、この計装の出力から来ています。

最適な測定指標は、linuxカーネルのバージョンとスケジューラ[4]によって変わる可能性があります

4.6 GitHub Actions は「マシンを 1 行で選べる」実験環境でもある

このチューニングの試行錯誤が現実的な工数で回ったのは、GitHub Actions では実行マシンの選択が runs-on の 1 行で済むからです。

runs-on: ubuntu-24.04-8core   # Larger Runner。2core にも 16core にも 1 行で変えられる
  • スペックの実験が容易: 「workers 5 は 4core で回るのか、8core が要るのか」を、ブランチで runs-on を書き換えて数ラン流すだけで比較できる。オンプレの CI サーバーだと、この比較のためだけにマシンを調達・構成することになります
  • ジョブごとに専有 VM: matrix の 10 ジョブはそれぞれまっさらな専有 VM を得るため、ジョブ間のリソース干渉(隣のジョブが CPU を食って flaky になる)を考えなくてよい。並列度チューニングの変数が「自分のジョブの workers 数」だけに絞れます
  • root 権限と新しめの kernel: sudo が使えるので、コンテナ内プロセスの /proc を読む I/O モニタや Docker コンテナの env 差し替え(PostgREST / Kong の再作成)のような「ホストへの介入」が全部書ける。PSI が使える kernel 4.20+ も標準イメージで満たされています

「計測を仕込む → マシンサイズと workers を変えて流す → Job Summary で比較する」のループが、インフラチームへの依頼なしに PR 上で完結する。これが並列度と Runner スペックを計測ベースで決められた実務上の理由です。

5. 変更差分から影響 E2E だけを回す「Impact Tests」

全量 6〜8 分とはいえ、アプリ実装のすべての PR で 10 ジョブ × 8core を回すのはコストが重い。そこで CI を 2 系統に分けています。

ワークフロー 契機 実行範囲
E2E Tests テストコード自体の変更 全量
E2E Impact Tests アプリ実装(src/**、migration、Edge Functions 等)の変更 影響範囲の spec のみ

影響判定は impact-map.json(変更ファイルパス → 影響 spec のルール集)と resolver スクリプトが担います。設計上のポイントは:

  • first-match 評価: 具体的なルールを先に、広いルールを後に
  • fail-open(安全側に倒す): どのルールにも当たらないファイルは「全量実行」。GitHub API の変更ファイル一覧が 3000 件で切り詰められた場合も全量にフォールバック
  • resolver 自体のユニットテストを毎回実行: 判定ロジックの退行は E2E では検知できないため
  • 判定根拠を PR に可視化: 「どのファイル変更がどのルールに当たり、どの spec を実行したか」を Job Summary に、影響を受けた URS(ユーザー要求仕様)の一覧を PR コメントに自動投稿

これで機能開発者は、自分の PR を出すだけで、影響範囲のリリース済み機能に対するリグレッションテストが 5 分前後で自動実行される体験になります。

6. flaky テストを作らないための「Iron Law」

高速な CI も、flaky で信頼を失えば意味がありません。私たちは flaky 対策を個人のノウハウにせず、E2E 作成スキル(後述)に「Iron Law」として明文化し、機械的に強制しています。特に効いているものを紹介します。

6.1 大原則: waitForTimeout 禁止、state-based wait のみ

固定 sleep は「遅い環境では足りず、速い環境では無駄」の代表格です。待つのは常に状態——ボタンの hidden、URL 遷移、レスポンス完了。


state-baseで数百msごとにpollingするメソッドと、固定時間経過後に1回だけ確認するwaitForTimeoutの安定感は大差


十分長い固定時間待機をするよりpollingする方が、平均すると時間効率も上回る

// モーダル submit の共通パターン
async submit(): Promise<void> {
  await this.requestButton.click();
  await this.requestButton.waitFor({ state: "hidden", timeout: 30000 });
  await new NotificationToast(this.page).waitDisappear();
}

また CI は --retries=0 で運用しています。リトライは flaky を隠蔽するだけで、非冪等操作の重複実行リスクもあるため、「落ちたら直す」を徹底する構えです。

6.2 不在判定は「陽性ランドマーク」を先に待つ(Law #7)

expect(locator).not.toBeVisible() は、要素が DOM に 0 個になった最初の瞬間に即成立します。ページがまだロード中で「これから描画される」要素も、その瞬間は 0 個なので誤通過します。

対策は、不在を主張する前に「もしその要素が描画されるなら、同時に描画され終わっているはずの陽性要素」の出現を待つこと。

// 「発効日編集ボタンが表示されない」ことの検証
await documentView.expectStatus("承認済み");          // ① 陽性ランドマークを待つ
await expect(documentView.effectiveDate).toBeVisible();
await expect(documentView.editButton).toHaveCount(0); // ② その上で不在を確認

not.toBeVisible()toHaveCount(0) に書き換えても同じ race を持つので救済になりません。ランドマーク待機だけが対策です。

6.3 同期 read + 静的 assert の禁止(Law #9)

// NG: snapshot 時点で値が未確定でも偶然通れば pass する
const text = await locator.innerText();
expect(text).toContain("承認済み");

// OK: 値が settle するまで自動ポーリングで待つ
await expect(locator).toContainText("承認済み");

innerText() / count() / isEnabled() などの同期 DOM read + 静的 expect は全面禁止し、auto-wait matcher に置き換えます。Playwright の auto-wait は「使うと flaky が減る便利機能」ではなく、同期 read を混ぜた瞬間に破綻する前提条件として扱っています。

6.4 「再取得しても表示が変わらない」レース(Law #11)

ソートやフィルタのような「API 再取得 → 再描画」を伴う操作には、auto-wait matcher でも防げない罠があります。再取得後の表示が直前と一致しうる場合(例: 別キーでソートしても並び順がたまたま同じ)、matcher は「すでに条件成立」とみなして API 反映前に即成立し、次の操作が stale な状態を読んでしまうのです。

対策として、操作の前にレスポンス待ちを仕掛けてから実行する共通ヘルパを Page Object 基底に用意しています。

async sortByColumn(column: SortColumn): Promise<void> {
  await this.runAndWaitForRpc("get_paginated_templates_with_sort", () =>
    this.sortButton(column).click()
  );
}

6.5 silent success は「busy の出現 → 消滅」を 2 段で待つ(Law #8)

完了 toast が出ないアクション(ファイルダウンロード、自動保存など)は、「処理中インジケータの出現」と「消滅」を両方 assert します。完了側だけだと「busy にならず即終了した(= 何もしていない)」ケースを見逃すためです。

6.6 事前データの精査(precheck)で「アプリのバグ」と「データ不備」を切り分ける

Factory で投入した事前データは、UI 操作を始める前に zod schema で shape 検証します(S0-Pre ステップ)。これが失敗すると [precheck] プレフィックス付きで落ちるため、「アプリがバグった」のか「テストの前提データが壊れていた」のかが、失敗メッセージだけで即座に判別できます。flaky 調査の最初の分岐を自動化する仕掛けです。

6.7 検証が通るだけでなく「人間が見られる証跡」を残す

全テストで動画を常時記録video: "on")し、ファイル往復系はアップロード元とダウンロード結果の実ファイルを、PDF の透かしなど Canvas 描画要素はスクリーンショットを、HTML レポートに添付します。中継モーダルのような一瞬で消える UI は、フェードイン完了を待ってから操作して動画にフレームが残るようにする、という徹底ぶりです。

これは flaky 調査の効率化と同時に、次章のバリデーション文脈で効いてきます。

7. E2E 開発・運用を支える AI スキル群

ここまでのルール群は、ドキュメントに書くだけでは守られません。私たちは Claude Code のスキル(プロジェクト共有のエージェント手順書)として運用に組み込んでいます。

スキル 役割
qms-e2e-author 対話型のテスト作成。正典レファレンス 4 本(アーキテクチャ/POM・spec 設計・seed/precheck・証跡/待機)の読了を強制し、Iron Law をセルフチェックリストとして通過しないと spec を書けないワークフロー
qms-e2e-impact-gate コミット前のローカルゲート。CI と同じ impact-map.json で変更差分から影響 spec を特定してローカル実行。CI に出す前にリグレッションを検知
qms-e2e-inspect ブラウザを操作して data-testid やフォーム構造を調査し、テスト作成の入力になる仕様ファイルを生成
qms-investigate テスト失敗の根本原因分析。仮説→検証→絞り込みのサイクルで flaky の真因を特定
qms-e2e-audit テスト整備状況を 4 軸 × 成熟度レベルで採点し、弱点領域を可視化

たとえば author スキルは、「必須情報(URS 手順・ロール展開・heavy 要否・事前データ)が揃うまで 1 行も書き始めない」「data-testid や Page Object の存在を grep で実在確認してから使う」「S0(事前データ投入)があれば precheck を必須実装する」といった制約をワークフローとして強制します。AI にテストを書かせる場合でも人間が書く場合でも、同じ品質バーを機械的に通す——これが 140 テストまでスケールしても品質が均質な理由だと考えています。

ローカル実行の影響判定マップ(impact-map.json)を CI と共有している点もポイントで、「ローカルゲートで通った範囲」と「CI が検証する範囲」が常に一致します。

8. 何が変わったか

開発フローへの統合

  • アプリ実装の PR → 影響範囲の E2E が約 5 分で自動実行され、影響 URS が PR コメントに載る
  • テストコードの PR → 全量 E2E が 6〜8 分で完走
  • コミット前 → ローカルゲートで同じ判定ロジックの事前チェック

E2E がユニットテストと同じ時間感覚で回るため、「E2E は夜間バッチで、結果は誰も見ない」という状態から、「PR のマージ条件として普通に機能する」状態になりました。

ソフトウェアバリデーション業務の補助

QMS 領域のソフトウェアには、規制対応としてのバリデーション(動作が要求仕様どおりであることの検証と記録)が求められます。

  • spec は URS(ユーザー要求仕様)単位で構成し、test.step のタイトルに URS の手順番号(S1 / E1…)を刻む
  • 全テストの動画・スクリーンショット・実ファイルが HTML レポートに証跡として残る
  • PR ごとに「この変更が影響した URS」が自動でトレースされる

つまり CI の 1 ランがそのまま「要求仕様 → テスト手順 → 実行証跡」のトレーサビリティを持った検証記録になり、リリース時のバリデーション作業を大きく補助しています。

9. まとめ

  • E2E の並列化は Playwright の設定ではなく、per-test のデータ独立・storageState・POM というテスト構造の問題。構造が正しければ workers はただの数字になる
  • 並列度は上げるほど、インフラの隠れた限界(コネクションプール、keep-alive 再利用レース)が露出する。計装を常設し、計測に基づいて project 単位で調整する
  • flaky 対策は個人技にせず Iron Law として明文化し、作成スキル(AI エージェント)のワークフローで機械的に強制する
  • 全量 6〜8 分 + 影響範囲実行の 2 系統で、E2E をユニットテストと同じ開発体験に載せる
  • 副産物として、URS トレーサビリティと動画証跡がソフトウェアバリデーションの記録として機能する

「E2E テストは遅くて壊れやすいもの」という常識は、構造・インフラ・規律・ツールの 4 点を揃えれば変えられる、というのが私たちの実感です。どれか 1 つでも参考になれば幸いです。

応募待っています

WEBエンジニア募集中です!医療業界での経験や3Dの知見は問いません。Berryでは様々な経験をお持ちの方が活躍できる環境があります。

Berryの考え方や製品に少しでも興味が持てた方はお気軽に応募下さい。

脚注
  1. [Playwright 公式ドキュメント「Page object models」(https://playwright.dev/docs/pom) ↩︎

  2. [Martin Fowler「PageObject」](https://martinfowler.com/bliki/PageObject.
    html) ↩︎

  3. カーネル公式ドキュメント「PSI - Pressure Stall
    Information
    ↩︎

  4. 現行の Linux カーネル(6.6 以降。ubuntu-24.04 ランナーのカーネル 6.8
    も該当)のEEVDF スケジューラ。アルゴリズムの原典は Stoica &
    Abdel-Wahab(1995)の論文「[Earliest Eligible Virtual Deadline First: A
    Flexible and Accurate Mechanism for Proportional Share Resource
    Allocation](https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=805
    acf7726282721504c8f00575d91ebfd750564)」、Linux
    実装の解説はカーネル公式ドキュメント「[EEVDF Scheduler](https://docs.kernel.or
    g/scheduler/sched-eevdf.html)」を参照。 ↩︎

株式会社Berry
株式会社Berry

Discussion