初投稿です。
個人開発の Web アプリに、E2E テストがゼロの状態から自習で Playwright を導入し、GitHub Actions に最小コストで載せた記録です。
目的は「まず CI で安定して動く最小構成」を作り、のちに拡張できる骨格を用意すること。自分用に知識のアウトプットを意識しています。
想定読者:E2E 未導入 or 失敗しがちな個人/小規模プロジェクトの方
得られること:差分実行・Chromium 限定・CI 専用 .env・早期スキップ・アーティファクト保存の最小セット
環境(ざっくり)
-
CI: GitHub Actions
ubuntu-latest - E2E: Playwright(JavaScript/TypeScriptどちらでも可)
- フロント: React(ビルド後を静的サーバで配信 or dev サーバ)
- バックエンド: 任意(例: Node/Laravel など)
-
Docker:
docker composeでフロント/バック/DB/E2E を起動 - ブラウザ: Chromium のみ(クロスブラウザは後日)
ポイントは 「CI 専用の .env」 を用意すること(下記参照)
ゴール(最小構成の定義)
- 差分実行:E2E ディレクトリに変更が無ければ 即スキップ
- Chromium 限定:テスト数 × ブラウザ数の乗算を避ける
- 早期チェック:重い Docker やビルドの 前に スキップ判定
- CI 専用 .env:権限やログ差で落ちない
- 失敗時の調査容易化:HTML レポート・スクショ・動画を保存
ディレクトリ例
project-root/
├─ src/frontend/
│ ├─ e2e/
│ │ ├─ auth/
│ │ │ └─ auth.spec.js
│ │ └─ consent/
│ │ └─ sample.spec.js
│ ├─ playwright-ci.config.js
│ └─ ...
├─ src/backend/ ...(任意)
├─ docker-compose.yml (dev/e2e override は任意)
└─ .github/workflows/e2e.yml
Playwright 設定(最小)
// src/frontend/playwright-ci.config.js
const { defineConfig, devices } = require('@playwright/test');
module.exports = defineConfig({
testDir: './e2e',
fullyParallel: false,
timeout: 600000, // 10分
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['junit', { outputFile: 'test-results/junit.xml' }],
['list'], // 進捗が分かる
],
use: {
baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000',
storageState: 'playwright/.auth/user.json',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
viewport: { width: 1280, height: 720 },
actionTimeout: 60000,
navigationTimeout: 120000,
waitForLoadState: 'domcontentloaded',
},
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.js/,
use: { storageState: { cookies: [], origins: [] } },
},
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
],
});
認証セットアップ例(storageState の最小)
// src/frontend/e2e/setup/auth.setup.js
const { test, expect } = require('@playwright/test');
test('global auth setup', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.E2E_EMAIL);
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD);
await page.getByRole('button', { name: 'Login' }).click();
await expect(page).toHaveURL(/\/dashboard|\/home/);
// 必要ならここで storageState を明示保存する実装に変更してOK
});
GitHub Actions(差分実行 × 早期スキップ)
# .github/workflows/e2e.yml
name: E2E Tests
on:
pull_request:
branches: [develop, main]
workflow_dispatch:
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v4
with:
fetch-depth: 0 # ← 差分検出に必須
- name: Check if E2E tests exist
id: check-e2e
run: |
if [ -d "src/frontend/e2e" ] && \
[ -n "$(find src/frontend/e2e -name '*.spec.*')" ]; then
echo "exist=true" >> $GITHUB_OUTPUT
echo "✓ E2E test directory found"
else
echo "exist=false" >> $GITHUB_OUTPUT
echo "⚠️ No E2E test files - skip"
fi
- name: Detect changed E2E files
id: changed
if: steps.check-e2e.outputs.exist == 'true'
run: |
BASE="${{ github.base_ref || 'develop' }}"
git fetch origin "$BASE":refs/remotes/origin/"$BASE" --no-tags
MB=$(git merge-base origin/"$BASE" HEAD) || { echo "::error::merge-base failed"; exit 0; }
CHANGED=$(git diff --name-only "$MB" HEAD -- 'src/frontend/e2e/**/*.spec.*' | sed 's|src/frontend/||g')
AUTH="e2e/auth/auth.spec.js"
if [ -z "$CHANGED" ]; then
echo "run=false" >> $GITHUB_OUTPUT
echo "files=" >> $GITHUB_OUTPUT
echo "⚠️ No E2E spec changed - skip heavy steps"
else
FILES=$(echo -e "$AUTH\n$CHANGED" | sort -u | tr '\n' ' ')
echo "run=true" >> $GITHUB_OUTPUT
echo "files=$FILES" >> $GITHUB_OUTPUT
echo "Run: $FILES"
fi
# ここから重い処理は条件付き
- name: Start containers (example)
if: steps.changed.outputs.run == 'true'
run: |
docker compose up -d --build
# フロント起動待ち(200 応答)
timeout 180 bash -c 'until curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 | grep -q 200; do echo "waiting frontend..."; sleep 5; done'
- name: Create CI .env for backend (optional)
if: steps.changed.outputs.run == 'true'
run: |
cat > src/backend/.env << 'EOF'
APP_ENV=test
APP_DEBUG=true
LOG_CHANNEL=stderr
CACHE_DRIVER=array
SESSION_DRIVER=array
QUEUE_CONNECTION=sync
API_VERSION=api/v1
EOF
- name: Run Playwright (only changed files + auth)
if: steps.changed.outputs.run == 'true'
run: |
cd src/frontend
npm ci
npx playwright install --with-deps
TESTS="${{ steps.changed.outputs.files }}"
echo "Running: $TESTS"
npx playwright test --config=playwright-ci.config.js $TESTS
- name: Upload E2E artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-${{ github.run_id }}
path: |
src/frontend/playwright-report/
src/frontend/test-results/
retention-days: 7
コツ:
Check if E2E tests existとDetect changed E2E filesを最初に置くと、差分が無い PR は重い処理を全部スキップできます。
CI 専用 .env(例:バックエンド)
CI はファイル権限差で落ちやすいので、ファイル I/O を避ける方針。
# src/backend/.env(CIのみ)
APP_ENV=test
APP_DEBUG=true
LOG_CHANNEL=stderr # ← ファイルに書かない
CACHE_DRIVER=array
SESSION_DRIVER=array
QUEUE_CONNECTION=sync
API_VERSION=api/v1
ローカル実行(最小手順)
# 1) アプリ起動(Docker/手動どちらでもOK)
docker compose up -d
# 2) フロント/バックが 200 応答になるまで待つ
curl -I http://localhost:3000
# 3) E2E 実行
cd src/frontend
npm ci
npx playwright install --with-deps
npx playwright test --config=playwright-ci.config.js
代表的なつまずき(チェックリスト)
-
404/タイムアウト:フロントの呼び先と API prefix(例
api/v1)の 一致 -
権限:CI ではファイルログを避ける(
LOG_CHANNEL=stderr) -
差分検出:
fetch-depth: 0→git merge-base→git diff - テスト数の暴増:テーブル駆動 × 複数ブラウザの乗算。CI は Chromium のみ
-
vendor/依存不足:コンテナ起動直後の
serveで落ちないよう、起動前に依存解決 - 待機不足:DB/フロントの起動を 必ず待つ(curl / mysqladmin ping)
実装ハイライト(本稿に含めた主な要素)
- 差分実行(変更なしで E2E をスキップ)
- Chromium 限定(最小で高速化)
- 早期チェック(重い処理の前にスキップ判定)
- CI 専用 .env(stderr ログ / array ドライバで安定)
- アーティファクト保存(HTML / スクショ / 動画で原因特定を高速化)
追記候補(必要に応じて):storageState の保存/再利用、
wait-for-*.shの共通化、compose ファイルの重ね順、Lint/Prettier の CI 設定
まとめ・振り返り
- ゼロ→イチは骨格優先:Chromium × 差分実行 × 早期スキップ で最短 “動く” を確保
-
CI とローカルは別設計:CI 専用
.env(LOG_CHANNEL=stderrなど)で落ちにくくする -
履歴が要:
fetch-depth: 0で正しい差分検出 -
見える化で速くなる:
listレポーター + アーティファクト保存
一番難しかったところ(所感)
-
ルーティング不一致と認証の不安定
- 原因:フロント
api/v1とサーバv1の齟齬など - 対処:API プレフィックス統一、setup project で認証 state を作って再利用
- 原因:フロント
-
CI 固有の権限問題
- 原因:
storage/logs/...への書き込みで Permission denied - 対処:ファイルに書かない(
LOG_CHANNEL=stderr)
- 原因:
-
差分検出の “no merge base”
- 原因:shallow clone
- 対処:
actions/checkout@v4にfetch-depth: 0を指定
今後の改善(短期)
- キャッシュ:npm / Docker 層のキャッシュ
- 優先度制御:重要 spec は常時、それ以外はナイトリー
- モニタリング:実行時間・失敗率の可視化
参考コマンド(コピペ用)
フロント起動待ち(200 応答待機)
timeout 900 bash -c '
until [ "$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000)" = "200" ]; do
echo "waiting frontend...";
sleep 5;
done
'
差分に含まれる E2E spec の一覧
BASE=origin/develop
git fetch origin develop:refs/remotes/origin/develop --no-tags
MB=$(git merge-base $BASE HEAD)
git diff --name-only "$MB" HEAD -- 'src/frontend/e2e/**/*.spec.*'
結論(短句)
- 最短で動かすなら:Chromium × 差分実行 × 早期スキップ
- 安定させるなら:stderr ログ × 起動待ち × CI 専用 .env
- 伸ばすなら:キャッシュ × 優先度制御 × モニタリング