8.6 KiB
Rustテストの設計規則
テストは、Rustの型だけでは保証できない製品の振舞いと、維持すべきコンパイル時のAPI制約を検証する。名前から守る規則が分かり、一つの失敗から原因となる契約を絞れるようにする。件数、配置場所、smoke testの成功を網羅性の根拠にしない。
検証範囲の選択
利用者が観測する振舞いと、起きた場合に問題となる失敗を先に定める。その失敗を検出できる最小の範囲を、影響・発生可能性・保守コストから選ぶ。「最小」は、mockに置き換えやすい範囲ではなく、必要な接続まで含む範囲である。
| 範囲 | 検証する内容 |
|---|---|
| 単体 | 周辺の仕組みを組み立てずに確認できる局所的な規則 |
| コンポーネント | 対象のサブシステムを、その境界から操作した振舞い |
| 結合 | 本番の構成要素を接続した際のデータ・権限・副作用の一致 |
| E2E | 選定した利用場面を、必要な製品インターフェースと実行境界を通して確認した結果 |
Cargoのintegration-test targetは別crateとしてコンパイルされ、公開APIを使う。この区分は可視性とコンパイル単位を決めるものであり、上の検証範囲とは別である。実装内のテストでも本番の構成要素を接続すれば結合テストとなる。
プロセス境界も検証レベルとは別である。接続の検証に必要な隔離子プロセスは通常の結合テストに含める。利用場面を試すために製品全体を組み立てる場合は、crate内に別の起動基盤を作らずtests/e2eを使う。
本番の接続と代替実装
結合テストには、入口、本番の構成要素、代替した境界、観測する結果を明示する。主張する接続は本番実装のままにし、その外側だけを代替する。双方を別々のfakeでテストしても、実際の組合せが動くことは証明できない。
永続化・トランザクションの保証には一時DBなどの実ストレージを使い、正本からの再読取りで結果を確認する。呼出し記録だけでは永続効果を証明できない。認証・権限を主張するテストでは、その境界をfixtureで迂回しない。障害注入でも、検証対象の復旧処理を代替しない。
再試行、競合、取消、復旧は製品の保証と失敗時の影響から選ぶ。全変更へ全障害の組合せを追加しない。通常は境界で決定的に失敗させ、プロセス死そのものが保証に関わる場合だけ終了を使う。論理的な復旧テストを、任意のOSクラッシュへの耐性の証拠にしない。
型の保証と外部契約
構築済みの型について、enumの値域、必須フィールドの存在、所有権、Optionの有無、網羅的なmatch、通常の引数・戻り値・trait適合を実行時に再検証するテストは追加しない。
JSON、TOML、DB行、HTTP、WebSocket、CLI引数、環境、パス、LLMのツール入力から型へ変換する境界は別である。Serde表現、schema、経路、範囲制限、拒否条件は外部契約として検証する。コンパイルの受理・拒否自体が公開APIの契約なら、macro展開、typestate、trait boundなどの最小のcompile-pass/compile-failテストを置く。
配置と所有者
| 範囲 | 配置 | 実行区分 |
|---|---|---|
| 単体 | 所有module内の#[cfg(test)] mod tests、またはsrc/配下の子テストmodule |
通常のcrateテスト |
| コンポーネント | 所有crateのsrc/<component>/tests.rsなど。公開APIを外部利用することが目的ならtests/ |
通常のcrateテスト |
| 結合 | 接続を所有するcrateのtests/。内部アクセスが必要なら結合範囲を明記したcrate内module |
対象の製品featureを含む通常のcrateテスト |
| E2E | Workspaceのtests/e2e packageと共有harness |
専用の明示選択target/job |
Cargo test-target間の補助処理はtests/common/mod.rs、crate内ではテスト支援moduleにまとめる。関連する結合ケースは同じtargetのmoduleに分け、ケースごとに実行ファイルを増やさない。公開範囲を広げてまで内部テストをtests/へ動かさない。
複数crateの結合も、接続の契約を所有するcrateに置く。専用packageは依存制約または実際の共有需要がある場合に限る。通常の結合テストを配置の都合でE2Eへ移さない。parser・ツール方針は所有module、保存制約はstore、製品の権限判断はauthority、routerとauthorityの接続はAPIの結合suiteが担当する。
共有fixtureと隔離
各結合・E2E suiteは、検証範囲、本番の構成要素、代替境界、実行target、前提条件を短いmoduleドキュメントに記す。資源の作成・解放を各ケースで重複させず、suiteの共有支援処理が次を所有する。
- RAIIで管理する一時ディレクトリ・DB、隔離した識別子・通信endpoint。開発者の稼働環境や実データを借りない。
- 明示的な実行ファイル、子のcwd・環境。親テストプロセスの環境を変更しない。
- 起動から終了・回収までの子プロセス管理。assert失敗やtimeoutでも後始末する。
- 観測可能なreadiness・イベントによる同期、待機の上限、障害診断の採取。
固定sleepで処理完了を推測しない。timeoutはハングを失敗へ変える上限として使う。通常テストは順序非依存・並列安全とし、live外部サービスや制限のないネットワークへ依存させない。通信には必要に応じてloopbackやIPCを使う。
環境変数は起動境界で一度だけsnapshotから型付き設定へ解決する。その変換を明示的snapshotでテストし、後続のロジックは型付き設定を受け取る。テスト制御・fixture・観測用の環境変数を追加せず、テストでプロセス全体の環境を読み書きしない。
名前とassertion
名前は<condition>_<expected_behavior>または<operation>_<expected_result>を基本とし、repository_path_rejects_symlink_escapeのように観測可能な規則を示す。works、test_parse、Ticket番号、private helper名だけでは規則を特定できない。boundary_、regression_、smoke_は意味がある場合だけ使い、必須接頭辞にはしない。
- fixtureには契約に必要な状態だけを用意する。境界テストには実際のargv、JSON、DB行、パスなどを含める。
- 結果、拒否、応答、永続記録、状態遷移をassertする。privateな呼出し順や偶然のIDに依存させない。
- 一つの入力規則は表形式にまとめ、別の製品規則は別のテストにする。
- APIは成功と拒否のstatus・body、ツールはLLMに見える名前・schema・検証・出力上限・権限・副作用を確認する。
- migrationは番号だけでなく、保存値、制約、変換、rollbackなど変更した保証を確認する。
- 完全文字列やsnapshot比較は、その表現自体が契約の場合に限る。大きなJSONでは意味をassertする。
- promptや組込み資源は所有者と包含条件を検証する。可変な一覧・件数・長い文言をassertionへ複写しない。
- 互換処理は明示的な製品判断がある場合だけ維持し、古いfixtureを通すためにaliasや旧経路を増やさない。
保守と実行責任
追加前に既存テストを確認し、各規則の主たるテストと接続を証明するテストを特定する。結合テストで局所的な全入力表を繰り返さない。バグ修正では既存テストを強化するか、継続的に守る規則を示す回帰テストを追加する。
製品契約が変わったら、opt-in suiteを含め、影響するfixtureとassertionを更新する。製品feature限定の契約は同じfeatureで選択する。通常のテストを無関係なテスト専用featureや環境変数で隠さない。
DB・socket・子プロセスが必要なだけではopt-inにしない。特殊なtool・権限・platformが必要なら、その前提と実行選択を定義する。この例外も自動的にE2Eを意味しない。target、CIの選択、timeout、cleanup、診断を共有harnessとrunnerの設定に反映する。
実行方法と結果の扱いは検証規則に従う。未実行、skip、未配線のCIを成功の証拠にしない。