---
paths: ["TIASshot.Tests/**", "TIASshot/**/*.cs"]
---

# テスト方針 (Testing Policy)

本プロジェクトのテストの種類・粒度・対象範囲・実行タイミングを定める．
具体的な回帰テスト手順は [TEST_01_テスト仕様](../../docs/06_TEST/TEST_01_テスト仕様.md) を参照する（本 rule は方針，TEST_01 は具体手順という役割分担）．

## 基本方針 (Basic Policy)

本プロジェクトは実機カメラ（Lucam / ImagingSource）・光源・ネイティブライブラリ（OpenCvSharp）に強く依存する一方で，画像処理・色補正・設定読み込み等のハードウェアに依存しない純粋ロジックも含む．

**目標は，人間による実機確認を「SDK の実呼び出し」と「実画像の見た目」だけに絞ること**である．そのため，実機でしか確かめられないものを最小限に切り分け，それ以外はすべて自動テストで担保する．コードを次の 4 層に分けてテストする．

| 層 | 対象 | テスト手段 |
| --- | --- | --- |
| ① ピュアロジック層 | ハードウェア・UI・スレッドに依存しない計算/変換ロジック | **NUnit で必ず自動テスト** |
| ② 結合ロジック層 | `CameraBase` の校正・撮影・保存の流れ，`Form1` の UI 連携メソッド | **NUnit＋偽カメラ＋ヘッドレス `Form1` で自動テスト**（後述「結合ロジック層のテスト」） |
| ③ SDK アダプタ層 | `IScam`・`Lucam`・`LightSource` | 判断ロジックを純粋関数に切り出して層①で検証する．SDK を呼ぶ部分そのものは層④（後述「SDK アダプタの書き方」） |
| ④ 実機層 | SDK の実呼び出し・実画像の見た目 | 実機テスト（`--hwtest`．ユーザーの合図があるときだけ）と手動テスト（TEST_01）．ここに残す項目は最小限にする |

「テストできない」と「テストしていない」を混同しない．実機が本当に要るのは SDK の実呼び出しと見た目だけであり，それ以外（状態遷移・スレッド連携・失敗時の通知等）は層②・③の手段でテストできる．「UI だから」「スレッドだから」を理由にテストを省かない．

※ 本方針（4 層モデル）は 2026-10-06 に旧方針（ピュアロジック層／ハードウェア結合層の 2 層）から改訂した．層②・③の基盤は [PLAN_05](../../docs/03_PLAN/PLAN_05_テスト網羅性向上計画.md) のフェーズに沿って整備中である．基盤が整うまでの間，層②・③に当たる変更は「後述の手段で書けるなら書く．書けないなら理由を明示して設計レビュー＋セルフテスト＋実機で担保する」とする．

標準に則る．NUnit の慣例（AAA: Arrange-Act-Assert）に従い，テストは実装のなぞり書きではなく**仕様（期待される入出力）を基準**に書く．

## アサーションの書き方 (Assertion Style)

**制約モデル（`Assert.That` ＋ `Is.*` / `Does.*`）のみを使う**．Classic assertion（`Assert.AreEqual`・`Assert.IsTrue`・`StringAssert.Contains` 等）は新規に書かないこと．

理由: NUnit 4 で Classic assertion は `Assert` から削除され `NUnit.Framework.Legacy.ClassicAssert` へ移動した．一方で制約モデルは NUnit 3 でも 4 でも同じく動くため，制約モデルに揃えておけば NUnit のメジャー更新にテストコードの変更なしで追従できる（既存の 209 箇所は 2026-08-28 に移行済み）．

| 用途 | 書き方 |
| --- | --- |
| 等値 | `Assert.That(actual, Is.EqualTo(expected))` |
| 浮動小数点の等値 | `Assert.That(actual, Is.EqualTo(expected).Within(delta))` |
| 真偽 | `Assert.That(x, Is.True)` / `Assert.That(x, Is.False)` |
| null | `Assert.That(x, Is.Null)` / `Assert.That(x, Is.Not.Null)` |
| 同一性 | `Assert.That(actual, Is.SameAs(expected))` |
| 部分文字列 | `Assert.That(actual, Does.Contain(expected))` |
| 例外 | `Assert.Throws<T>((Action)(() => ...))` / `Assert.DoesNotThrow((Action)(() => ...))` |

- **例外系のラムダは `(Action)` にキャストする**．NUnit 4.6 は `TestDelegate` 版（`[Obsolete]`）と `Action` 版（`[OverloadResolutionPriority]`）の両方を持ち，LangVersion 7.3 のテストプロジェクトでは優先度属性が解釈されず CS0121（呼び出しが曖昧）になる．LangVersion は古い VS / Build Tools との互換のため上げない（2026-09-29 の NUnit 4.6.1 移行で 16 箇所を対応）．

- **引数の順序に注意する**．Classic は `(expected, actual)` だが制約モデルは `(actual, 制約)` である．とくに `Does.Contain` は第 1 引数が**検査対象**で，期待する部分文字列は制約の側に入る（逆にすると意味が反転したまま通ってしまう場合がある）．
- 浮動小数点の比較で `.Within(...)` を落とさないこと．厳密比較になりフレーキーになる．

## テストの種類 (Test Types)

| 種類 | 対象 | 自動化 | 実行タイミング |
| --- | --- | --- | --- |
| 単体テスト | 層①（層③から切り出した判断ロジックを含む） | 自動（NUnit） | コミット前・`/implement` Phase 2 |
| 結合ロジックテスト | 層② | 自動（NUnit．`[Category("Integration")]`） | コミット前・`/implement` Phase 2 |
| ビルドテスト | ソリューション全体 | 自動（MSBuild） | 全コミット前 |
| 回帰テスト | 色補正・チャート検出の出力（合成チャート，および実機で録画したチャート・白板のフレーム） | 自動（基準データ比較） | リファクタ・修正の前後 |
| セルフテスト | 依存ロード・偽カメラでのプレビュー経路・合成チャートでの校正〜保存 | 自動（`--selftest`．`/verify`・`/implement` Phase 1b） | 依存更新・層②〜④の変更後，手動テストの前段 |
| 実機テスト | 層④のうち数値で判定できる SDK の実呼び出し（接続・プレビュー・露光/ゲイン・光源・閉じる・再接続．`--with-chart` では実チャートでの校正・撮影と保存・保存中/校正中に閉じる） | 自動（`--hwtest`．Claude が実機を操作する．**ユーザーの合図があるときだけ**） | 層③・④の変更後，手動テストの前段 |
| 手動テスト | SDK の実呼び出し・実画像の見た目（層④） | 手動（実機） | 機能追加・修正の完了時 |

※ 実機録画データ（舌診チャート・白板のフレーム）による回帰は PLAN_05 のフェーズ 4 で整備する．**舌の画像は個人情報のためテストデータに含めない**．

回帰テスト・手動テストの具体手順は [TEST_01_テスト仕様](../../docs/06_TEST/TEST_01_テスト仕様.md) に記載する．

## テスト対象の切り分け (Test Scope)

どのクラス・処理を自動テストの対象とするかを明示する．新しいコードを追加した際は，この表に従って対象/対象外を判断する．

### 層①: ピュアロジック層

| 対象 | テスト内容の例 |
| --- | --- |
| `ColorCorrection/ColorCorrector` | 次元拡張（`ExtendMat`）・変換行列算出・色変換の入出力．既知の入力 Mat に対する出力 Mat／数値の検証 |
| `ChartDetection/ChartDetector` | 既知のチャート画像を入力したときのマスク位置・検出結果の幾何的検証 |
| `Config` | 設定 XML のパース・型変換・範囲フィルタ（例: チャネル数の `4〜17` 絞り込み） |
| `Utils/IoUtil` | CSV の読み書き往復（`SaveMatToCsv` → `LoadMatFromCsv`）で値が保たれること．保存先フォルダ・ファイル名の組み立て |
| `Utils/ImageStats`・`Utils/BitmapUtil` | 画像統計・JSON 出力，Bitmap の複製（所有権を含む） |
| ゲイン更新比率の計算（`GetRatio`・`GainCalc` 相当） | 入力値・目標値・更新レートに対する比率計算．非有限値・範囲端の扱い |
| 層③から切り出した判断ロジック | ROI とフレームサイズの照合，ゲインの算出，バッファサイズの検査，例外を通知に変える処理 等 |

### 層②: 結合ロジック層

| 対象 | テスト内容の例 |
| --- | --- |
| `CameraBase` の校正 | チャート検出 → 固定判定 → 変換行列算出 → 撮影ボタン有効化の状態遷移．途中で失敗したときに再検出できる状態へ戻ること |
| `CameraBase` の撮影・保存（`RunShotLoop`・`SaveThread`・`SaveImages`） | 単発・連続撮影で期待どおりのファイルが保存されること．保存失敗時に成功と表示しないこと．残りフレームの破棄・解放 |
| `CameraBase` の切断・終了 | 保存中・校正中に切断したときの後始末の順序 |
| `UI/Form1` の UI 連携メソッド | `ShowMessage`・`ShowError`・`EnableShots`・`GetDataName` 等の結果．UI スレッド外から呼んだときのマーシャリング |

### 自動テスト対象外（層④: 実機層）

| 対象 | 対象外の理由 |
| --- | --- |
| `IScam`・`Lucam` の SDK 呼び出し（接続・露光/ゲイン設定・フレーム取得・ストリーム停止） | カメラ SDK（`dll.*` / `TIS.Imaging.*`）と実機に依存する．判断ロジックは層③として切り出して層①で検証する |
| `LightSource` のシリアル通信 | 光源（実機）に依存する．ポートを開けなかったときの扱い等の判断は層③として切り出す |
| 画面描画の見た目（`Form1`・`PreviewMonitor` の表示内容） | 目視でしか判断できない．表示用 Bitmap の所有権・更新の流れはセルフテスト（`preview`）で担保する |

層④のうち次の項目は，実機テスト（[PLAN_06](../../docs/03_PLAN/PLAN_06_実機自動テスト計画.md)．`python scripts/verify/run_hwtest.py --camera <lucam|iscam> [--with-chart]`．`HWTEST` フラグ）で Claude が実機を操作して自動で確認できる．**ユーザーが会話の中で合図したときだけ**実行し，合図が無ければ「検証不能（合図なし）」とする（手順・しきい値は `.claude/verify-profile.md` の「実機テスト」節）．

| 項目 | 確認できること |
| --- | --- |
| `connect` | 指定した機種に接続でき，機種名・シリアル番号が取れる |
| `preview` | プレビューが流れる（フレームレートの下限・本モニタへの表示） |
| `exposure-gain` | 露光・ゲインの設定がフレームの明るさに期待どおりの向きで効き，元の値に戻せる |
| `light` | 光源の点灯・消灯が明るさに効き，ポートを閉じた後に開き直せる |
| `close-preview` | プレビュー中に閉じる処理を呼んでもすぐ戻り，後始末の順序が正しい |
| `reconnect` | 切断した後に再接続でき，フレームが届く |
| `calibrate`（`--with-chart`） | 実物の舌診チャートで自動校正が完了し，校正後のフレームを変換した色票が参照値に近い（8bit・チャネルごとの差） |
| `shot`（`--with-chart`） | 単発・連続撮影で撮影画像・sRGB 変換画像・Info.csv が保存され，撮影時刻の間隔が設定どおり |
| `close-saving`（`--with-chart`） | 保存中に閉じようとすると取り消され，保存の完了後は閉じられる（後始末の順序） |
| `close-calibrating`（`--with-chart`） | 再接続して校正中に閉じる処理を呼んでもすぐ戻る |

※ 2026-10-08 時点で実機テストを通したのは ImagingSource だけ（Lumenera の実機テストは行わない方針．PLAN_06「対象機種」）．チャートが要る項目（`--with-chart`．PLAN_06 フェーズ 2）も同日に実物のチャートで合格した．`--with-chart` はチャート不要の検査を行わないため，両方を続けて実行する．変換画像の見た目（17 次元の変換の点状のノイズ等．PLAN_06「17 次元の変換のノイズ」）は数値で判定しきれないため，引き続き手動で確認する．

※ 層④に手を入れた場合は**設計レビュー＋実機回帰**で挙動を確認する．その前段として，実機なしで通せる範囲はセルフテスト（`python scripts/verify/run_selftest.py`．`.claude/verify-profile.md`）で，上表の実機テストの項目はユーザーの合図のもと実機テストで確認できる．`/implement` の Tester フェーズでは，対象が層④だけの場合に限り，テストを書かずに設計レビューで判断してよい．層②・③に当たる部分が含まれるなら，その部分のテストは書く．

## 結合ロジック層のテスト (Integration Logic Tests)

層②のテストは，カメラの代わりに偽カメラ（`SelfTest/SelfTestCamera`．`CameraBase` の派生）を，フォームの代わりにヘッドレス `Form1`（`Form1.SelfTest.cs`）を使う．どちらも `SELFTEST` フラグ（Debug 系の構成のみ）でコンパイルされるため，層②のテストも `#if SELFTEST` で囲む．

- **STA スレッドで動かす**: WinForms のコントロールと `Control.Invoke` は STA スレッドとメッセージ処理を必要とする．テストは共通ハーネス（`TIASshot.Tests/IntegrationHarness.cs`．使用例は `CameraBaseSaveIntegrationTests`）を通して，STA スレッド上でヘッドレス `Form1` を生成し，`SelfTestStart` でハンドルを作ってから使う．ハーネスを経由せずに個別のテストでスレッドやメッセージ処理を組まない
- **待機には必ずタイムアウトを付ける**: 「条件が満たされるまでメッセージを処理して待つ」処理は無限待ちにしない．タイムアウトしたらテストを失敗させ，ハングで後続のテストを止めない．層②の各テストには **1 件あたり 15 秒**のタイムアウトを付ける
- **実行時間の目安**: 層②のテスト全体で **30 秒以内**を目安にする．テスト用の一時設定では校正フレーム数（`Calib/Frames`）等を小さくし，1 件あたり 1 秒前後で終わるようにする．目安を超えそうなときは，テストを減らしたり `/verify` から外したりせず，テスト用設定を見直して速くする（`/auto-audit` は修正 1 件ごとにテスト一式を何度も回すため，ここが延びると全作業が遅くなる）
- **観測はアクセサ経由で行う**: 結果は `Form1.SelfTest.cs` のアクセサ（`SelfTestMessage`・`SelfTestLastError`・`SelfTestShotsEnabled` 等）と，保存されたファイルで確かめる．`Form1` の private フィールドをリフレクションで読まない．足りないアクセサは `Form1.SelfTest.cs` に追加する
- **失敗の注入を必ず含める**: 各観点で正常系に加え，失敗を注入するケースを 1 つ以上書く（存在しない保存先，壊れた設定ファイル，チャートが検出できない画像，途中で例外を投げる偽カメラ 等）．不具合は異常系に集中しやすい
- **一時ファイルの扱い**: 設定は一時フォルダに複製した `config.xml` を `Config.Load(path)` で読み込み，保存先も一時フォルダにする．同梱の `config.xml` や本番の保存先（`C:\TIAS_Data` 等）には書き込まない．テストの終わりに一時フォルダを削除する
- **カテゴリ**: 層②のテストには `[Category("Integration")]` を付ける（所要時間の把握のため）．メモリ量で判定するテスト（リーク再現・ベンチ）には `[Category("Memory")]` を付ける（環境による不安定さが出たときに通常の実行から外せるようにするため）
- **常に実行する**: 層②のテストは `/verify`（`run_selftest.py`）・`/auto-audit`・`/implement` のテスト実行に常に含める．CI が無く，これらが実質唯一の自動チェックのため，カテゴリで分けて普段は回さない運用にはしない
- **安定性の確認**: 層②のテストを新しく追加したときは，取り込む前に 3 回連続で実行して緑になることを確認する

## SDK アダプタの書き方 (Writing SDK Adapters)

層③（`IScam`・`Lucam`・`LightSource`）に手を入れるときは，次の形に寄せる（Humble Object パターン）．

- SDK を呼ぶメソッドは「SDK から受け取った値を判断ロジックに渡し，その結果を SDK に渡す」だけの薄い形にする
- 判断（値の検証・計算・分岐・例外を通知に変える処理）は SDK 型に依存しない純粋関数として切り出し，層①でテストする
- 切り出しの対象例: ROI とフレームサイズの照合，ホワイトバランスのゲイン算出（`GainCalc` のように），フレームバッファのサイズ検査，ポートを開けなかったときの扱い
- 既存コードを切り出したときは，切り出した直後に実機確認を 1 回行う（切り出し自体で SDK 呼び出しの順序や値を変えていないことの確認）

## フレームワークと配置 (Framework & Layout)

- **フレームワーク**: NUnit（`NUnit3TestAdapter` で Visual Studio / `dotnet test` から実行）．
- **テストプロジェクト**: `TIASshot.Tests`（**x64 / .NET Framework 4.8**．本体と同一ターゲット）．
- **ネイティブ依存**: `ColorCorrector` 等のテストは OpenCvSharp のネイティブ DLL をロードするため，テストプロジェクトも **x64 必須**かつ OpenCvSharp ランタイムを参照すること（プラットフォームを `AnyCPU` にすると Mat 演算が実行時に失敗する）．
- **配置**: ソリューションルート直下にテストプロジェクトを置く．

```text
TIASshot/                  ← ソリューションルート
├── TIASshot.sln
├── TIASshot/             ← 本体プロジェクト
└── TIASshot.Tests/       ← テストプロジェクト（NUnit）
    ├── ColorCorrectorTests.cs
    ├── ConfigTests.cs
    └── ...
```

- **命名**: テストクラスは `<対象クラス>Tests`，テストメソッドは「対象_条件_期待結果」が分かる名前にする（日本語名も可）．
- **粒度**: 1 テストにつき 1 つの検証観点とする．1 メソッドで多数の `Assert` を詰め込まない．
- **テストデータ**: 入力画像・基準 CSV 等は `TIASshot.Tests` 配下の専用フォルダ（例: `TestData/`）に置き，テストから相対参照する．
  - 実機録画フレーム（舌診チャート・白板）は **Git LFS** で管理する．可逆形式（PNG）で保存し，1 機種・1 場面あたり 3〜5 フレームに絞る．校正にはこれを繰り返し流して使う
  - **舌の画像はテストデータに含めない**（個人情報のため）

## テストの実行 (Running Tests)

テストプロジェクトは本体に合わせたレガシー形式（packages.config）のため `dotnet test` は使えない．ビルド後に `vstest.console.exe` を **x64** で実行する（ネイティブ DLL を x64 でロードするため）．

**具体的なコマンドは [ENV_04_開発コマンド](../../docs/02_ENV/ENV_04_開発コマンド.md) を参照する**（本 rule は方針，ENV_04 はコマンドという役割分担）．MSBuild・`vstest.console.exe` の場所は VS のエディション（Community / Professional / Enterprise / Build Tools）によって変わるため，パスを直書きせず ENV_04 の `vswhere` による解決手順を使うこと．

**エージェント（tester・refactorer 等）は ENV_04 の「テストランナー」（`python scripts/verify/run_tests.py`）を使う**．ビルドからテスト実行・結果の集計までを 1 コマンドで行い，`--filter`（カテゴリ等の絞り込み）と `--repeat`（安定性確認の連続実行）に対応する．オーケストレーターにテストの実行を代行させない．

Visual Studio 上ではテストエクスプローラーから実行できる（プラットフォームを x64 にすること）．

## モック方針 (Mocking)

- **過剰なモックを避ける**．ピュアロジックは実値（実際の Mat・数値）で検証する．
- **静的依存の扱い**: `Config` のような静的クラスへの依存がテストの妨げになる場合，テスト容易化のための最小限のシーム（引数化・依存の注入）を導入してよい．ただし**本番の挙動は変えない**こと．
- **ハードウェア SDK はモックしない**．SDK の型や関数を偽物に差し替えてテストするのではなく，判断ロジックを SDK から切り出してテストする（層③）．
- **偽カメラはモックではなく代替実装として使う**．`SelfTestCamera` は `CameraBase` の派生として実際の校正・撮影・保存の流れを通すもので，SDK を模倣するものではない．失敗を注入したい場合は，偽カメラ（またはテスト用の派生クラス）に失敗させる口を足してよい．ただし本番のコード（`IScam`・`Lucam`）の挙動は変えない．

## カバレッジ方針 (Coverage)

- **数値的なカバレッジノルマは課さない**．カバレッジ率の達成自体を目的化しない．
- 代わりに，**重要な経路を網羅する**ことを定性的な目標とする．具体的には，色変換行列の算出・次元拡張・CSV 往復・ゲイン更新比率といった「誤ると出力が壊れる中核ロジック」と，校正・撮影・保存の流れ（正常系と失敗時）にテストを行き渡らせる．
- **カバレッジは抜けを見つける地図として使う**．テストプロジェクトに導入済みの `Microsoft.CodeCoverage` で「一度も通っていない箇所」を確認し，そこが重要な経路なら優先してテストを足す．

## 既存ドキュメントとの関係 (Relation to Existing Docs)

- 本 rule（`.claude/rules/testing.md`）は**テストの方針**を定める．
- [TEST_01_テスト仕様](../../docs/06_TEST/TEST_01_テスト仕様.md) は**回帰テスト・手動テストの具体手順**（基準データの作成・比較方法・合格条件）を定める．
- 自動テスト（単体テスト）の具体的なケースが増えた場合は，`docs/06_TEST/` に追記する（本 rule には方針のみ残し，個別ケースは重複させない）．
