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

反模式

  1. 報告太細沒人看 — 50 個 metric 在 PR 留言 → 直接被滑過。3-5 個關鍵指標就夠
  2. 每次新增留言 — PR 變一坨「Test Report 1」「Test Report 2」。一定要 update 舊的。
  3. fail 沒附證據 — 只說「test_login_2 fail」沒 screenshot/trace → debug 要爬 artifact,沒人爬。
  4. 跨 run 沒比較 — 看不到「上次也 fail 嗎」→ 不知道是 regression 還是 flaky。Allure 趨勢解這個。
  5. 報告跟 CI 解耦 — 報告生在某個服務上、CI fail 也不擋 merge → quality gate 失效。

最後

報告整合的價值是 「降低看 PR 的人理解測試結果的成本」。從「點 4 層才看到」變「PR 上就看到」, review 速度直接快一倍。每次省 dev 30 秒 × 一天 20 個 PR = 一年 30 小時。值得花一天設定。