概要
Azure DevOpsのパイプラインの上でE2Eテストをしたかった。
前回、認証をパイプラインで動かすのに失敗したので、CI環境では認証はスキップするつくりにした。
それでもなかなか動かなかったので、試行錯誤した記録を残す。
ソースコード
packages/bdd-e2e-test/azure-pipeline.yml
trigger:
branches:
include:
- id/75/bddOnPipeline
jobs:
- job: bdd_test
displayName: 'Run BDD E2E tests'
pool:
vmImage: 'ubuntu-latest'
services:
postgres:
image: postgres:17.4-alpine
ports:
- 5432:5432
env:
POSTGRES_USER: test_user
POSTGRES_PASSWORD: postgres
POSTGRES_DB: main
options: >-
--health-cmd="pg_isready"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- checkout: self
- task: UseNode@1
inputs:
version: '22.x'
- script: |
curl -fsSL https://bun.sh/install | bash
displayName: 'Install Bun'
- script: echo "##vso[task.setvariable variable=PATH]$(HOME)/.bun/bin:$(PATH)"
displayName: 'Add Bun to PATH'
- script: bun install
displayName: 'Install Dependencies'
- script: npx playwright install --with-deps chromium
workingDirectory: apps/frontend
displayName: 'download new browsers'
- script: cp .dev.vars.ci .dev.vars
workingDirectory: apps/backend
displayName: 'Setting Backend env vars'
- script: cp .env.sample .dev.local
workingDirectory: apps/frontend
displayName: 'Setting Frontend env vars'
- script: |
bun run dev > backend.log 2>&1 &
echo $! > ../backend.pid
workingDirectory: apps/backend
displayName: 'Start Backend'
# ホットリロードなど不要なので、bun run dev ではなく bun run preview を使う
- script: |
bun run preview:ci > frontend.log 2>&1 &
echo $! > ../frontend.pid
workingDirectory: apps/frontend
displayName: 'Start Frontend'
- script: |
echo "Waiting for frontend server..."
for i in {1..20}; do
if curl -s http://localhost:5173 > /dev/null; then
echo "Frontend server is up."
break
fi
sleep 2
done
echo "Waiting for backend server..."
for i in {1..20}; do
if curl -s http://localhost:8787 > /dev/null; then
echo "Backend server is up."
break
fi
sleep 2
done
displayName: 'Wait for servers to start'
- script: bun run migrate:azci
workingDirectory: packages/database
displayName: 'Migration RDB'
- script: bun run bdd-test
workingDirectory: packages/bdd-e2e-test
displayName: 'Run E2E Tests'
env:
CI: true
# 調査用にE2Eテストのスクリーンショットやcontentsを確認できるようにする
- task: PublishBuildArtifacts@1
condition: always()
inputs:
pathToPublish: 'packages/bdd-e2e-test/output'
artifactName: 'e2e-output'
displayName: 'Upload screenshots as artifacts'
# 調査用にバックエンドのログを確認できるようにする
- task: PublishBuildArtifacts@1
condition: always()
inputs:
pathToPublish: 'apps/backend/backend.log'
artifactName: 'backend-log'
displayName: 'Upload screenshots as artifacts'
- script: |
kill $(cat backend.pid)
kill $(cat frontend.pid)
displayName: 'Cleanup Servers'
workingDirectory: apps
condition: always()
実行結果
調査記録
バックエンドのログ
PublishBuildArtifactsを使う。
- script: |
+ bun run dev > backend.log 2>&1 &
echo $! > ../backend.pid
workingDirectory: apps/backend
displayName: 'Start Backend'
# 調査用にバックエンドのログを確認できるようにする
- task: PublishBuildArtifacts@1
condition: always()
inputs:
+ pathToPublish: 'apps/backend/backend.log'
artifactName: 'backend-log'
displayName: 'Upload screenshots as artifacts
Publishをクリックすると
フロントエンド
DEBUG=pw:api
でデバッグログを確認できる
packages/bdd-e2e-test/package.json
"bdd-test": "DEBUG=pw:api cucumber-js --config cucumber.mjs --exit"
ヘッドレスブラウザのログは下記のようにBefore
でイベントをひろう。
packages/bdd-e2e-test/e2e/step-definitions/scenario.steps.ts
Before(async function (this) {
const browser = await chromium.launch({
headless: process.env.CI === 'true',
}); // headless: true にするとブラウザが表示されない
const context = await browser.newContext();
this.page = await context.newPage();
// ヘッドレスブラウザ―のコンソール出力をキャッチする
this.page.on('console', (msg: any) => {
if (msg.type() === 'error') {
console.error(`[Browser Console Error]: ${msg.text()}`);
} else if (msg.type() === 'warning') {
console.warn(`[Browser Console Warning]: ${msg.text()}`);
} else {
console.log(`[Browser Console]: ${msg.text()}`);
}
});
this.page.on('request', (request: any) =>
console.log(`Request: ${request.method()} ${request.url()}`),
);
this.page.on('response', (response: any) => {
console.log(`Response: ${response.status()} ${response.url()}`);
if (response.status() >= 400) {
console.error(`Error Response: ${response.status()} ${response.url()}`);
}
});
});
スクリーンショットを取ったり、page.content
でHTMLを出力したりで調査できる。
packages/bdd-e2e-test/e2e/step-definitions/scenario.steps.ts
When(
'ユーザーが「 {string} 」リンクをクリックする',
async function (this, text) {
const { page } = this;
await page.screenshot({
path: 'output/screenshots/debug.png',
fullPage: true,
});
const content = await page.content();
fs.writeFileSync('output/debug.html', content);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
await page.screenshot({
path: 'output/screenshots/after_networkidle.png',
fullPage: true,
});
fs.writeFileSync('output/after_networkidle.html', await page.content());
await page.waitForSelector(`a:has-text("${text}")`);
await page.getByRole('link', { name: text }).nth(0).click();
},
);
参考
余談
エラー検証中に下記のエラーがなかなか難しかった。
dbの接続を使いまわしていたら出たエラーだった。修正こみ
Error: Cannot perform I/O on behalf of a different request. I/O objects (such as streams, request/response bodies, and others) created in the context of one request handler cannot be accessed from a different request's handler. This is a limitation of Cloudflare Workers which allows us to improve overall performance. (I/O type: Writable)
追記: Neon Proxyのログ
- script: |
# ログファイルパスを定義
LOG_DIR=$(pwd)/neon-proxy-logs
mkdir -p $LOG_DIR
echo "Starting Neon Proxy..."
docker run -d --name neon-proxy \
-e PG_CONNECTION_STRING=postgres://postgres:postgres@host.docker.internal:5432/main \
-p 4444:4444 \
--add-host=host.docker.internal:host-gateway \
ghcr.io/timowilhelm/local-neon-http-proxy:main
# バックグラウンドでログ収集を開始
docker logs -f neon-proxy > $LOG_DIR/neon-proxy.log 2>&1 &
LOGGER_PID=$!
echo $LOGGER_PID > $LOG_DIR/neon.pid
# Proxy が立ち上がるまで待機
echo "Waiting for Neon Proxy..."
for i in {1..20}; do
if curl -s http://db.localtest.me:4444/sql &>/dev/null; then
echo "Neon Proxy is ready"
break
fi
sleep 2
done
displayName: 'Start Neon Proxy'
# Neon Proxyログをアーティファクトとして保存するためのタスク
- task: PublishBuildArtifacts@1
condition: always()
inputs:
pathToPublish: 'neon-proxy-logs'
artifactName: 'neon-proxy-logs'
displayName: 'Upload Neon Proxy logs as artifacts'
- script: kill $(cat neon.pid)
displayName: 'Cleanup Neon Proxy logs'
workingDirectory: neon-proxy-logs
condition: always()