Test Report 整合 — JUnit XML、Allure、PR 自動留言的選擇與設定
跑完測試只看 console pass/fail,是業餘水準。好的報告整合 = 開發看 PR 就知道發生什麼、QA 看儀表板就知道哪邊脆弱。這篇講四種格式的選擇與實作。
為什麼要整合
| 場景 | 沒整合 | 有整合 |
|---|---|---|
| PR review | 開發點 Actions 找 log | PR 上直接顯示 pass/fail/duration |
| Fail debug | 翻 log 找 stack trace | 點報告直接到 fail step + screenshot |
| 趨勢觀察 | 看不到 | 過去 30 天 pass rate / flaky rate |
| 找弱點 | 沒資料 | 哪個 module 最常 fail / 哪個 case 最慢 |
四種報告格式
| 格式 | 設定難度 | 強項 | 弱項 |
|---|---|---|---|
| JUnit XML | ⭐ 最簡單 | CI 工具普遍支援 | 純資料、要工具解析 |
| GitHub Checks | ⭐⭐ 中 | 直接顯示在 PR | GitHub-only |
| Allure | ⭐⭐⭐ 高 | 最漂亮、最詳細 | 要架服務、學 attachment API |
| 自訂 PR 留言 | ⭐⭐ 中 | 完全可控 | 自己寫腳本 |
新團隊建議:先 JUnit + PR 留言 → 穩定後再上 Allure。
1. JUnit XML(通用語言)
所有 CI 工具都吃 JUnit XML。先有這個,其他都能套。
各框架輸出 JUnit
# Playwright
# playwright.config.ts:
reporter: [['junit', { outputFile: 'results.xml' }]]
# pytest
pytest --junitxml=results.xml
# Jest
jest --reporters=default --reporters=jest-junit
# package.json:
"jest-junit": { "outputDirectory": ".", "outputName": "results.xml" }
# Cypress
# cypress.config.ts:
reporter: 'junit',
reporterOptions: { mochaFile: 'results.xml' }
# Go test
go test -v ./... | go-junit-report > results.xml
GitHub Actions 顯示
- name: Upload test results
if: always()
uses: dorny/test-reporter@v1
with:
name: Tests
path: results.xml
reporter: java-junit # 或 jest-junit / dotnet-trx
fail-on-error: false
效果:PR 旁邊 Checks 多一個「Tests」,點進去看 fail 列表 + stack trace。不用點 Actions 就看到。
2. GitHub Checks(PR 上直接顯示)
不用第三方 action,用 actions/github-script 寫自己的 check:
- name: Create check
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const { passed, failed, skipped } = JSON.parse(
fs.readFileSync('summary.json', 'utf8')
);
const conclusion = failed > 0 ? 'failure' : 'success';
await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'E2E Tests',
head_sha: context.sha,
status: 'completed',
conclusion,
output: {
title: `${passed} passed, ${failed} failed, ${skipped} skipped`,
summary: `Run on ${context.workflow}`,
},
});
效果:PR 上的 check list 看到「E2E Tests ✓ 47 passed, 0 failed」。可以設成 required check(merge 不過要過這個)。
3. Allure Report(漂亮 + 詳細)
Allure 是最強的測試報告 — 附 screenshot、video、attachments、retry history、趨勢圖。但要架 Allure server 才能看趨勢。
Playwright + Allure
npm install -D @playwright/test allure-playwright allure-commandline
// playwright.config.ts
reporter: [['allure-playwright', { outputFolder: 'allure-results' }]]
GitHub Actions 設定
- name: Run tests
run: npx playwright test || true
- name: Get Allure history
uses: actions/checkout@v4
if: always()
continue-on-error: true
with:
ref: gh-pages
path: gh-pages
- name: Generate Allure report
if: always()
uses: simple-elf/allure-report-action@master
with:
allure_results: allure-results
allure_history: allure-history
keep_reports: 30 # 留最近 30 次
- name: Deploy to GitHub Pages
if: always()
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_branch: gh-pages
publish_dir: allure-history
效果:https://<org>.github.io/<repo>/<run-number>/ 看到完整 Allure 報告,含趨勢。
Allure 進階功能
- Step 內塞 attachment
typescript await allure.attachment('Request payload', JSON.stringify(payload), 'application/json'); - Severity — 標 critical / normal / minor
typescript allure.severity('critical'); - TMS / Bug 連結 — 連到 Jira
typescript allure.tms('PROJ-123', 'https://jira.example.com/PROJ-123');
4. 自訂 PR 留言(最彈性)
要顯示什麼自己決定。範例:fail case list + flaky 警告 + duration 排行
- name: Comment PR
if: github.event_name == 'pull_request' && always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const r = JSON.parse(fs.readFileSync('summary.json'));
const failedList = r.failed_tests.length > 0
? r.failed_tests.slice(0, 10).map(t => `- ❌ \`${t.title}\` (${t.error.split('\\n')[0]})`).join('\n')
: '_no failures_';
const slowest = r.tests
.sort((a, b) => b.duration - a.duration)
.slice(0, 5)
.map(t => `- \`${t.title}\` ${(t.duration / 1000).toFixed(1)}s`)
.join('\n');
const body = `## 🧪 Test Report
| Status | Count |
|--------|-------|
| ✅ Passed | ${r.passed} |
| ❌ Failed | ${r.failed} |
| ⏭ Skipped | ${r.skipped} |
| 🕐 Duration | ${(r.duration / 60000).toFixed(1)} min |
### Failed tests
${failedList}
### Slowest tests (top 5)
${slowest}
📊 [Full report](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
// 找舊留言更新,不要每次新增
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const old = comments.data.find(c =>
c.user.login === 'github-actions[bot]' && c.body.includes('🧪 Test Report')
);
if (old) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: old.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
效果:PR 上一個固定留言、每次更新。不會洗版。
多 job 報告合併
E2E 開 4 個 shard 跑,每個 shard 一份報告,要合併才有意義。
Playwright blob reporter(官方)
// playwright.config.ts
reporter: process.env.CI ? [['blob']] : 'html',
# 每個 shard upload blob
- uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shard }}
path: blob-report/
# 一個 merge job 下載全部
- uses: actions/download-artifact@v4
with:
pattern: blob-report-*
merge-multiple: true
path: all-blob-reports
- run: npx playwright merge-reports --reporter html ./all-blob-reports
JUnit XML 合併
npm install -g junit-merge
junit-merge -d ./reports/ -o merged.xml
或 Python:
pip install junitparser
junitparser merge reports/*.xml merged.xml
Coverage 報告整合
不是測試結果但相關。
Codecov
- uses: codecov/codecov-action@v4
with:
files: ./coverage/coverage-final.json
token: ${{ secrets.CODECOV_TOKEN }}
PR 自動留言 coverage diff。
Coveralls
- uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
怎麼選
| 情境 | 推薦組合 |
|---|---|
| 新團隊、簡單需求 | JUnit + GitHub Checks |
| 需要附 screenshot、video | JUnit + 自訂 PR 留言(含 artifact link) |
| 大團隊、要趨勢 | Allure on GitHub Pages |
| 多 stage 測試(unit + e2e) | 每個 stage JUnit、最後合併留言 |
| 跨 repo 集中看 | ReportPortal / TestRail |
反模式
- 報告太細沒人看 — 50 個 metric 在 PR 留言 → 直接被滑過。3-5 個關鍵指標就夠。
- 每次新增留言 — PR 變一坨「Test Report 1」「Test Report 2」。一定要 update 舊的。
- fail 沒附證據 — 只說「test_login_2 fail」沒 screenshot/trace → debug 要爬 artifact,沒人爬。
- 跨 run 沒比較 — 看不到「上次也 fail 嗎」→ 不知道是 regression 還是 flaky。Allure 趨勢解這個。
- 報告跟 CI 解耦 — 報告生在某個服務上、CI fail 也不擋 merge → quality gate 失效。
最後
報告整合的價值是 「降低看 PR 的人理解測試結果的成本」。從「點 4 層才看到」變「PR 上就看到」, review 速度直接快一倍。每次省 dev 30 秒 × 一天 20 個 PR = 一年 30 小時。值得花一天設定。