---
title: Test Report 整合 — JUnit XML、Allure、PR 自動留言的選擇與設定
description: 把 unit / integration / E2E 測試結果整合到 CI、PR 留言、團隊儀表板。JUnit / Allure / GitHub Checks / 自訂報告的取捨。
category: automation
tags: [test-report, junit, allure, github-checks, cicd]
date: 2026-06-09
---

# 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

```bash
# 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 顯示

```yaml
- 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**：

```yaml
- 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

```bash
npm install -D @playwright/test allure-playwright allure-commandline
```

```typescript
// playwright.config.ts
reporter: [['allure-playwright', { outputFolder: 'allure-results' }]]
```

### GitHub Actions 設定

```yaml
- 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 排行**

```yaml
- 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（官方）

```typescript
// playwright.config.ts
reporter: process.env.CI ? [['blob']] : 'html',
```

```yaml
# 每個 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 合併

```bash
npm install -g junit-merge
junit-merge -d ./reports/ -o merged.xml
```

或 Python：

```bash
pip install junitparser
junitparser merge reports/*.xml merged.xml
```

## Coverage 報告整合

不是測試結果但相關。

### Codecov

```yaml
- uses: codecov/codecov-action@v4
  with:
    files: ./coverage/coverage-final.json
    token: ${{ secrets.CODECOV_TOKEN }}
```

PR 自動留言 coverage diff。

### Coveralls

```yaml
- 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 小時。值得花一天設定。
