# CSP-005　單元測試與品質規範（Unit Test & Quality）

| 項目 | 內容 |
|---|---|
| 文件編號 | CSP-005 |
| 版本 | 1.0 |
| 狀態 | 已核定 |
| 撰寫者 | SA / QA Lead |
| 審查者 | PMO / TestBOT |
| 對應需求 | NFR-QUAL 品質群組；BRD-004 POC 驗收情境 |

---

## 1. 目的

定義**單元測試的撰寫標準、覆蓋門檻、品質閘與禁止事項**，確保交付程式碼具備可驗證的正確性。
本規範與 TRP-001（測試計畫）互補：TRP-001 管「驗收測試劇本」，本規範管「開發期單元測試」。

---

## 2. 測試金字塔

```mermaid
flowchart TD
    U[Unit 單元測試 ~70%] --> I[Integration 整合 ~20%]
    I --> E[End-to-End / POC 驗收 ~10%]
    style U fill:#EEF3F8,stroke:#0B2E59
    style E fill:#FFF6E0,stroke:#C8A45C
```

- **單元測試**：不啟動 Spring 容器、不連 DB；以純邏輯測 `domain/` 與 `application/` handler。
- **整合測試**：以 Testcontainers 起 PostgreSQL / Kafka，驗證 `infrastructure/` 真實接線。
- **E2E / POC**：對應 BRD-004 的 17 項情境，經 api-gateway 端到端跑。

---

## 3. 單元測試撰寫約定

1. **框架**：JUnit 5（`@Test`）、AssertJ（斷言）、Mockito（**僅用於隔離外部依賴**，不得 mock 被測邏輯本身）。
2. **命名**：`<受測方法>_<情境>_<預期>`。例：`approve_whenLimitSufficient_returnsApproved()`。
3. **結構**：遵守 Given-When-Then；每個測試只驗證**一個**行為。
4. **資料**：測試內的 `given` 資料**合法**（真實格式的卡號、金額），不得用 `null` 或無意義假值掩飾。
5. **邊界**：每個業務規則至少 3 例（正常值、下界、上界／例外）。

---

## 4. 覆蓋率與品質閘（CI 強制）

| 指標 | 門檻 | 工具 |
|---|---|---|
| 行覆蓋率（line） | ≥ 80% | JaCoCo |
| 分支覆蓋率（branch） | ≥ 70% | JaCoCo |
| Mutation 存活率 | ≤ 25%（即殺死率 ≥ 75%） | PITest |
| 靜態分析 | 0 error | Checkstyle / SpotBugs / PMD |
| 禁止模式掃描 | 0 命中 | scan_scaffold / scan_mockdata |

> **門檻為合併前紅線**：任一未達標，CI 失敗、PR 不得合併。

### 4.1 量測方式（2026-10-02 補；`#A-160`）

| 項目 | 內容 |
|---|---|
| 工具接線 | `backend/build.gradle.kts` 之 `subprojects {}` 內 `apply(plugin = "jacoco")`（**由 `tools/gen_service_skeleton.py` 之 `T_ROOT_BUILD` 產生——改動須兩邊都動**） |
| 產出 | 每個模組 `build/reports/jacoco/test/html/`（人看）＋ `build/reports/jacoco/test/jacocoTestReport.xml`（**供 `tools/verify_nfr.py` 解析**） |
| 執行 | `cd backend && ./gradlew test jacocoTestReport` |
| 門檻任務 | `jacocoTestCoverageVerification` **已定義**（行 0.80／分支 0.70）；⚠️ **尚未接入 `check`**——待基線補足後開啟（見 `TRP-002 §7.2`） |
| Mutation | PITest（≥75%）**尚未接入**——PIT 執行成本高，另立任務 |

#### 4.1.1 ★ 排除規則（**刻意從嚴**）

**僅排除「機器產生」與「純啟動類」兩類**，其餘**一律計入**：

| 排除樣式 | 理由 |
|---|---|
| `**/ErrorCodes.class` | 由 `tools/gen_error_codes.py` **產生**（依 `SDD-006 §3.4`）——非人手撰寫 |
| `**/*Application.class` | Spring Boot 啟動類（無業務邏輯） |

> ⚠️ **刻意不排除 DTO／View／Request／Config**——業界慣例常排除之，但那會**虛胖覆蓋率**。
> 本專案之 `record` 雖多為資料載體，**仍可能有驗證與正規化邏輯**（例：`SpecialLimit#coversMcc`）。
> **寧可數字難看，不要數字騙人**（`GOV-007` 反假綠）。
> 如日後確有排除需求，**須逐類具名並於本節載明理由**，不得以「`**/dto/**`」通吃。

> ⚠️ **未接入 `check` 之門檻＝目前不會擋 PR**——此為**已知之過渡狀態**，
> 目的在「**先量到真實基線**」再設門檻；`TRP-002 §7.2 #1` 已列此缺口。

### 4.2 ★★ 實測基線（2026-10-02，`#A-160`）

| 指標 | 實測 | 門檻 | 判定 |
|---|---:|---:|:--:|
| **行覆蓋率** | **53.7%**（8,034／14,970） | ≥ 80% | ❌ **未達** |
| **分支覆蓋率** | **46.8%**（2,164／4,623） | ≥ 70% | ❌ **未達** |

> ★★ **此數字不是計法造成的假象**：已做對照——若改採業界慣例（排除
> `*View／*Request／*Response／*Dto／*Config／*Properties` 等），結果為 **行 53.8%／分支 46.7%**，
> **與本節計法幾乎相同（差 0.1 pp）**。→ **低覆蓋率源自「真實邏輯缺測試」**。
> 逐模組數據、最低三者與後續建議見 `docs/08-evidence/N1_工具缺口補齊與量測結果.md §1`。

### 4.3 依賴漏洞掃描（`#A-160` 新增）

| 項目 | 內容 |
|---|---|
| 後端工具 | **`tools/scan_deps.py`**（OSV；零相依；以 Gradle init script 列舉 `runtimeClasspath`，**不改 repo**） |
| 前端工具 | `npm audit` |
| 判定 | **有 High／Critical 或嚴重度未知 → 退出碼 1**（**fail-closed**） |
| **實測** | 後端 **3 CRITICAL＋10 HIGH**（Tomcat 11.0.24／Jackson 2.21.5·3.1.5）；前端 **1 HIGH＋3 MODERATE** |
| **修補（`CR-037`，2026-10-05）** | ✅ **Tomcat 11.0.24 → 11.0.26**（3 個 CRITICAL **歸零**） |
| **修補（`CR-038`，2026-10-05）** | ✅ **Jackson 2.21.5 → 2.21.7／3.1.5 → 3.1.7**（**10 個 HIGH 歸零**） |
| **修補（`CR-040`，2026-10-05）** | ✅ **前端 `vite 5.4.21 → 8.3.2`／`react-router-dom 6.30.6 → 7.18.4`／`@vitejs/plugin-react 4.3.4 → 6.1.1`**（**HIGH 1 → 0；MODERATE 3 → 0**） |
| **現況** | **後端 CRITICAL 0／HIGH 0／MODERATE 0**（`CR-037`／`CR-038`／`CR-039`）＋ **前端 0**（`CR-040`）→ ★★ **`npm audit` 與 OSV 掃描皆 0 命中** |
| 對準出之影響 | `TRP-001 §6.2` 第 7 條「**安全掃描無高／嚴重漏洞**」→ ★ **後端與前端皆達成**（⚠️ **滲透測試**仍屬外部環境，見 `TRP-002 §5`） |

> ⚠️ **升版前須做版本相容性查證**（`CSP-006` 之版本為單一真實來源）——**不得**直接改版號。

---

## 5. 測試資料策略

- **不依賴共享資料庫狀態**：每個測試自給自足（arrange 自己建資料），以 `@BeforeEach` 重置或使用
  獨立 schema。
- **整合測試**用 Testcontainers 起一次性容器，測完即銷毀；禁止連「開發／測試共享真庫」跑單元測試。
- **個資**：測試資料一律使用合成卡號（如 `4000********1234` 之測試卡 bin），禁止真實客戶資料。

---

## 6. 禁止事項（客戶硬性要求：不得有「假測試」）

| 編號 | 禁止 | 說明 |
|---|---|---|
| T1 | **假斷言**：`assertTrue(true)`、`assertEquals(1,1)` 之類無效驗證 | 偽裝通過，毫無意義 |
| T2 | **吞失敗**：`@Test` 內 `try{...}catch{ /*ignore*/ }` | 隱藏錯誤 |
| T3 | **mock 取代真實邏輯**：`when(service.calc()).thenReturn(42)` 然後只驗證 `42` 被回傳，卻沒測 `calc()` 本身 | 應直接對 `calc()` 做單元測試 |
| T4 | **`@Disabled` 長期停用**而不開 issue 追蹤 | 測試債務累積 |
| T5 | **塞框架**：測試類只有 `setUp` 無任何 `@Test` | 空殼（對應 CSP-001 B1） |
| T6 | **依賴執行順序**：測試間有隱含次序才過 | 不可重現 |

**掃描器**：`cardcore/tools/scan_faketest.py` 於 CI 偵測 T1/T3/T5 模式（如 `assertTrue(true)`、
`verifyNoInteractions` 卻無 `when`、類別無 `@Test` 方法），命中即紅燈。

---

## 7. 與 POC 驗收之連結

BRD-004 之 17 項 POC 情境，每一項對應至少一組 E2E 測試（命名 `Poc<NN>_*.test`），
其通過狀態直接回寫 GOV-004 追溯矩陣。未通過之 POC 測試，標記 `BLOCKED` 並開缺陷單（TRP-001 §5）。

---

## 8. 交付前自檢清單（Coder 提交 PR 前）

- [ ] 單元測試通過、覆蓋率達標
- [ ] 無 T1–T6 禁止模式
- [ ] 靜態分析 0 error
- [ ] 無 `TODO`/`FIXME`（CSP-001 B5）
- [ ] 提交訊息含 `Refs: FR/NFR-XXX`
- [ ] 新增 API 已同步 `contracts/openapi/*.yaml`
