# 帳票設計書 4-junit

## 概要

本ドキュメントは、Node.jsテストランナーの「JUnitレポーター」の設計仕様を記載したものである。テスト結果をJUnit XML形式で出力するレポーター機能の実装詳細、出力形式、処理フローについて説明する。

### 本帳票の処理概要

JUnitレポーターは、Node.jsテストランナーが実行したテスト結果を、JUnit XML形式で標準出力に表示する帳票である。JUnit XML形式はJenkins、CircleCI、GitLab CI、Azure DevOps等の多くのCI/CDツールでサポートされている標準的なテスト結果フォーマットであり、テスト結果の可視化、トレンド分析、レポート生成に広く利用される。

**業務上の目的・背景**：CI/CDパイプラインにおいて、テスト結果を構造化された形式で記録・分析する必要がある。JUnit XML形式は事実上の業界標準であり、ほぼすべてのCI/CDツールがこの形式をネイティブにサポートしている。この形式でテスト結果を出力することで、テスト結果のダッシュボード表示、失敗トレンドの追跡、テスト実行時間の分析などが可能になる。

**帳票の利用シーン**：
- Jenkins、CircleCI、GitLab CI等でのテスト結果レポート生成
- Azure DevOpsでのテスト結果の可視化
- テスト結果の長期保存と傾向分析
- 複数テストスイートの結果統合
- テスト品質メトリクスの収集

**主要な出力内容**：
1. XMLヘッダーとtestsuitesルート要素
2. testsuite要素（テストスイート情報）
3. testcase要素（個別テストケース）
4. failure要素（失敗詳細）
5. skipped要素（スキップ/TODO情報）
6. 診断メッセージ（XMLコメント）
7. 実行時間、テスト数、失敗数等のメトリクス

**帳票の出力タイミング**：
- テストランナー開始時にXMLヘッダーとtestsuites開始タグを出力
- 各テストの完了時に内部ツリー構造を更新
- 全テスト完了後にツリーをXMLに変換して出力
- 最後にtestsuites終了タグを出力

**帳票の利用者**：
- CI/CDシステム管理者
- DevOpsエンジニア
- テスト自動化エンジニア
- プロジェクトマネージャー（テストメトリクス確認）

## 帳票種別

テストレポート / XML形式（JUnit XML）

## 利用画面

| 画面No | 画面名 | URL/ルーティング | 出力操作 |
|--------|--------|-----------------|---------|
| - | CLIターミナル | `node --test --test-reporter=junit` | テスト実行コマンド |

## 出力形式

### 基本仕様

| 項目 | 内容 |
|-----|------|
| ファイル形式 | XML（JUnit XML） |
| 用紙サイズ | N/A（テキスト出力） |
| 向き | N/A |
| ファイル名 | N/A（標準出力へ直接出力） |
| 出力方法 | 標準出力（stdout） |
| 文字コード | UTF-8 |

### XML固有設定

| 項目 | 内容 |
|-----|------|
| XMLバージョン | 1.0 |
| エンコーディング | utf-8 |
| ルート要素 | testsuites |
| インデント | タブ × (nesting + 1) |

## 帳票レイアウト

### レイアウト概要

JUnitレポーターは、テスト階層を内部ツリーとして構築し、最後にXML形式で出力する。

```
┌─────────────────────────────────────┐
│          XMLヘッダー部              │
│  <?xml version="1.0" ...?>         │
│  <testsuites>                      │
├─────────────────────────────────────┤
│          テストスイート部           │
│  <testsuite name="..." tests="5">  │
│    <testcase name="..." time="...">│
│      <failure type="..." .../>     │
│    </testcase>                     │
│    <!-- diagnostic comment -->     │
│  </testsuite>                      │
├─────────────────────────────────────┤
│          終了タグ部                 │
│  </testsuites>                     │
└─────────────────────────────────────┘
```

### XMLヘッダー部

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | XML宣言 | XMLバージョンとエンコーディング | 固定値 | `<?xml version="1.0" encoding="utf-8"?>` |
| 2 | testsuites開始 | ルート要素開始タグ | 固定値 | `<testsuites>` |

### testsuite要素

| No | 属性名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | name | テストスイート名 | data.name | 文字列 |
| 2 | time | 実行時間（秒） | duration_ms / 1000 | 小数点6桁 |
| 3 | disabled | 無効テスト数 | 固定(0) | 数値 |
| 4 | errors | エラー数 | 固定(0) | 数値 |
| 5 | tests | テスト総数 | 子要素数 | 数値 |
| 6 | failures | 失敗数 | 失敗子要素数 | 数値 |
| 7 | skipped | スキップ数 | スキップ子要素数 | 数値 |
| 8 | hostname | ホスト名 | os.hostname() | 文字列 |

### testcase要素

| No | 属性名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | name | テストケース名 | data.name | 文字列 |
| 2 | time | 実行時間（秒） | duration_ms / 1000 | 小数点6桁 |
| 3 | classname | クラス名 | data.classname ?? 'test' | 文字列 |
| 4 | file | ファイルパス | data.file | 文字列（オプション） |

### failure要素

| No | 属性名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | type | 失敗タイプ | error.failureType \|\| error.code | 文字列 |
| 2 | message | エラーメッセージ | error.message.trim() | 文字列 |
| 3 | 内容 | エラー詳細 | inspectWithNoCustomRetry(error) | テキスト |

### skipped要素

| No | 属性名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | type | スキップタイプ | 'skipped' または 'todo' | 文字列 |
| 2 | message | スキップ理由 | data.skip または data.todo | 文字列 |

## 出力条件

### 抽出条件

| 条件名 | 説明 | 必須 |
|-------|------|-----|
| テスト結果イベント | test:start/pass/fail/diagnostic | Yes |

### ソート順

| 優先度 | 項目 | 昇順/降順 |
|-------|------|---------|
| 1 | イベント発生順 | 昇順（時系列） |

### 改ページ条件

なし（XML構造として出力）

## データベース参照仕様

### 参照テーブル一覧

本帳票はデータベースを参照しない。テストランナーからのイベントストリームを入力として使用する。

| テーブル名 | 用途 | 結合条件 |
|-----------|------|---------|
| N/A | - | - |

### イベントデータ構造

#### test:start イベント

| 参照項目 | 帳票項目との対応 | 取得条件 | 備考 |
|---------|----------------|---------|------|
| data.name | testsuite/testcase name | - | ツリーノード作成 |
| data.nesting | ネストレベル | - | 親子関係構築用 |

#### test:pass / test:fail イベント

| 参照項目 | 帳票項目との対応 | 取得条件 | 備考 |
|---------|----------------|---------|------|
| data.name | testcase name | - | - |
| data.nesting | ネストレベル | - | - |
| data.details.duration_ms | time属性 | - | 秒に変換 |
| data.details.error | failure要素 | test:fail時 | - |
| data.classname | classname属性 | - | デフォルト'test' |
| data.file | file属性 | - | オプション |
| data.skip | skipped要素 | skip時 | - |
| data.todo | skipped要素 | todo時 | type='todo' |

#### test:diagnostic イベント

| 参照項目 | 帳票項目との対応 | 取得条件 | 備考 |
|---------|----------------|---------|------|
| data.message | XMLコメント | - | `<!-- message -->` |
| data.nesting | 親ノード特定 | - | - |

## 計算仕様

### 計算項目一覧

| 項目名 | 計算式 | 端数処理 | 備考 |
|-------|-------|---------|------|
| time | duration_ms / 1000 | 小数点6桁固定 | NumberPrototypeToFixed |
| tests | nonCommentChildren.length | N/A | コメント除外 |
| failures | ArrayPrototypeFilter(children, isFailure).length | N/A | 失敗ノードをカウント |
| skipped | ArrayPrototypeFilter(children, isSkipped).length | N/A | スキップノードをカウント |

## 処理フロー

### 出力フロー

```mermaid
flowchart TD
    A[テストランナー開始] --> B[XMLヘッダー出力]
    B --> C[testsuites開始タグ出力]
    C --> D[イベント待機]
    D --> E{イベントタイプ判定}
    E -->|test:start| F[startTest: ツリーノード作成]
    E -->|test:pass/fail| G[テスト結果処理]
    E -->|test:diagnostic| H[コメントノード追加]
    F --> D
    G --> I{子ノードあり?}
    I -->|Yes| J[testsuite要素として設定]
    I -->|No| K[testcase要素として設定]
    J --> L[メトリクス計算]
    K --> M{失敗?}
    M -->|Yes| N[failure子要素追加]
    M -->|No| O{skip/todo?}
    O -->|Yes| P[skipped子要素追加]
    L --> D
    N --> D
    P --> D
    O -->|No| D
    H --> D
    D --> Q[イベント終了]
    Q --> R[ルートノードをXML変換]
    R --> S[testsuites終了タグ出力]
```

### startTest関数処理

```mermaid
flowchart TD
    A[test:startイベント] --> B[現在のスイートを保存]
    B --> C[新しいノード作成]
    C --> D{親スイートあり?}
    D -->|Yes| E[親の子配列に追加]
    D -->|No| F[rootsに追加]
    E --> G[currentSuiteを更新]
    F --> G
```

### treeToXML関数処理

```mermaid
flowchart TD
    A[ツリーノード受信] --> B{文字列?}
    B -->|Yes| C[エスケープして返却]
    B -->|No| D{コメント?}
    D -->|Yes| E[XMLコメント生成]
    D -->|No| F{子要素あり?}
    F -->|No| G[自己閉じタグ生成]
    F -->|Yes| H[開始タグ + 子要素 + 終了タグ生成]
```

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 表示メッセージ | 対処方法 |
|----------|---------|--------------|---------|
| XMLエスケープ | 特殊文字含有 | エスケープ処理 | escapeAttribute/Content/Comment関数 |
| 構造不整合 | test:startなしでtest:pass/fail | - | 自動的にrootノードを作成 |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定データ件数 | メモリに全ノードを保持（ツリー構造） |
| 目標出力時間 | 全テスト完了後に一括出力 |
| 同時出力数上限 | 1（シングルストリーム） |

## セキュリティ考慮事項

- テスト名、エラーメッセージに含まれる機密情報がXMLにエスケープされて出力される
- ファイルパスがfile属性として出力され、ディレクトリ構造が露出する可能性がある
- ホスト名がhostname属性として出力される
- XMLエスケープにより、インジェクション攻撃は防止される

## 備考

- 非同期ジェネレータ関数として実装
- テスト階層をツリー構造として内部保持し、最後にXMLに変換
- os.hostname()でホスト名を取得（HOSTNAME定数として保持）
- 子要素を持つテストはtestsuite、持たないテストはtestcaseとして出力
- 診断メッセージはXMLコメントとして出力

---

## コードリーディングガイド

本帳票を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

内部ツリー構造とXML要素の対応を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | junit.js | `lib/internal/test_runner/reporter/junit.js` | ツリーノードの構造（attrs, children, tag, nesting） |

**読解のコツ**: 内部ツリー構造はXML要素と1対1で対応している。tag属性がtestsuiteかtestcaseかで出力形式が変わる。

#### Step 2: エントリーポイントを理解する

junitReporter非同期ジェネレータ関数の構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | junit.js | `lib/internal/test_runner/reporter/junit.js` | junitReporter関数全体（64-163行目） |

**主要処理フロー**:
1. **65行目**: XMLヘッダー出力
2. **66行目**: testsuites開始タグ出力
3. **70-85行目**: startTest関数 - ツリーノード作成
4. **87-158行目**: イベントループ
5. **89-91行目**: test:start - startTest呼び出し
6. **93-147行目**: test:pass/fail - テスト結果処理
7. **149-154行目**: test:diagnostic - コメントノード追加
8. **159-161行目**: ルートノードをXML変換
9. **162行目**: testsuites終了タグ出力

#### Step 3: ツリー構築ロジックを理解する

test:pass/failイベントでのツリー構築とタグ決定ロジックを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | junit.js | `lib/internal/test_runner/reporter/junit.js` | test:pass/fail処理（93-147行目） |

**主要処理フロー**:
- **95-101行目**: currentSuiteがない場合やマッチしない場合の処理
- **106行目**: time属性の計算（duration_ms / 1000, 6桁固定）
- **107-115行目**: 子要素がある場合はtestsuite
- **116-145行目**: 子要素がない場合はtestcase

#### Step 4: XML生成ロジックを理解する

treeToXML関数とエスケープ関数を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | junit.js | `lib/internal/test_runner/reporter/junit.js` | treeToXML関数（33-54行目） |
| 4-2 | junit.js | `lib/internal/test_runner/reporter/junit.js` | エスケープ関数（21-31行目） |

**主要処理フロー**:
- **21-23行目**: escapeAttribute - 属性値のエスケープ（改行→&#10;, "→&quot;）
- **25-27行目**: escapeContent - コンテンツのエスケープ（&→&amp;, <→&lt;）
- **29-31行目**: escapeComment - コメントのエスケープ（--→&#45;&#45;）
- **33-54行目**: treeToXML - ツリーノードをXML文字列に変換

#### Step 5: ユーティリティ関数を理解する

失敗/スキップ判定関数を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | junit.js | `lib/internal/test_runner/reporter/junit.js` | isFailure関数（56-58行目） |
| 5-2 | junit.js | `lib/internal/test_runner/reporter/junit.js` | isSkipped関数（60-62行目） |

**主要処理フロー**:
- **56-58行目**: isFailure - failure子要素またはfailures属性で判定
- **60-62行目**: isSkipped - skipped子要素またはskipped属性で判定

### プログラム呼び出し階層図

```
node --test --test-reporter=junit
    │
    ├─ lib/internal/test_runner/runner.js
    │      └─ parseCommandLine() → getReportersMap()
    │              └─ require('internal/test_runner/reporter/junit')
    │
    └─ lib/internal/test_runner/reporter/junit.js
           │
           ├─ junitReporter (async generator)
           │      ├─ startTest() [ツリー構築]
           │      ├─ treeToXML() [XML変換]
           │      │      ├─ escapeAttribute()
           │      │      ├─ escapeContent()
           │      │      └─ escapeComment()
           │      ├─ isFailure()
           │      └─ isSkipped()
           │
           ├─ hostname()
           │      └─ os
           │
           └─ inspectWithNoCustomRetry()
                  └─ lib/internal/errors.js
```

### データフロー図

```
[入力]                    [処理]                           [出力]

テストイベント ───▶ junitReporter ───▶ 内部ツリー構造
  ストリーム              │
                         ├─ test:start → ノード作成
                         ├─ test:pass → ノード更新（testcase）
                         ├─ test:fail → ノード更新 + failure追加
                         └─ test:diagnostic → コメントノード追加
                                │
                         全イベント完了
                                │
                         treeToXML() ───▶ XML文字列 ───▶ 標準出力
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| junit.js | `lib/internal/test_runner/reporter/junit.js` | ソース | JUnitレポーターのメイン実装 |
| utils.js | `lib/internal/test_runner/utils.js` | ソース | kBuiltinReporters登録 |
| errors.js | `lib/internal/errors.js` | ソース | inspectWithNoCustomRetry関数 |
| os.js | `os` | Node.js組込み | hostname()関数 |
| runner.js | `lib/internal/test_runner/runner.js` | ソース | テストランナーのメイン処理 |
