rust-testing について
rust-testingは、検証する振る舞いに応じたテスト方法の選択を支援するRust開発者向けスキルです。一般的なテスト形式、ゴールデンファイルテスト、移植時の差分テストに加え、決定性と検証の指針を扱います。
主な機能
- 01標準的な5種類のテスト形式を選択
- 02テーブル駆動のケースと明確な失敗情報
- 03エラーのバリアントと独立した期待値を検証
- 04決定性を保ちネットワークを使わないテスト
- 05Rust移植に差分テストを適用
活用シーン
- 01Rustの変更に合うテスト形式を選ぶ
- 02エラー動作の回帰テストを追加
- 03Rust移植版を既存実装と比較
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rewrite-rs/ skills --skill rust-testingインストール後、Claude Code で/rust-testingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
DIFFERENTIAL-TESTING.mdTEST-DESIGN.mdagents/openai.yaml詳細
Rust テスト
テストスイートは、チームがどのような変更を黙って受け入れないかを示すものです。このスキルでは、何をどの形式でテストする価値があるかを決めます。通常の5つの形式、ファイル形式の成果物に対するゴールデンファイル、移植に対する差分テストが対象です。テスト対象のコードをどのような形にすべきかは決めません。コードの形を整えるのはクラフトスキルの役割です。変更をテストするかどうかをレビュー判定として決めるのは /rust-code-review です。
テストの目的
テストは、動作が変わったときに失敗するために存在します。実装を言い換えただけのテスト――たとえば、ゲッターが渡されたフィールドを返すと確認するテスト――は、コードがリファクタリングされたときに失敗し、動作が壊れたときに通ります。これはまさに本末転倒です。テストを書く前に、そのテストが検出する回帰を一文で言い表してください。その文が「コードが変わった」なら、そのテストは書かないでください。
何を検証するかに応じて形式を選ぶ
形式は習慣ではなく、検証内容に合わせて選びます。各形式の詳細――ファイルの配置場所、アクセスできる範囲、コスト――については TEST-DESIGN.md を参照してください。各形式の要点は次のとおりです。
| 形式 | 使用する場面 |
|---|---|
#[cfg(test)] mod tests 内のユニットテスト | 非公開の要素にアクセスする必要がある場合、または興味深い境界条件を持つ純粋関数をテストする場合 |
tests/ 内の統合テスト | 実際の呼び出し元が目にする公開 API を検証する場合――利用者と同じようにクレートをインポートします |
| ドキュメントテスト | 例が何よりもまずドキュメントである場合。実質的にはユニットテストのドキュメントテストは mod tests に置きます |
プロパティテスト(proptest) | 入力空間全体で成り立つ規則を検証する場合――往復変換、不変条件、パーサーとシリアライザーの組み合わせ |
スナップショットテスト(insta) | 出力が大きく、構造化されていて、目視でレビューする場合――レンダリング済みテキスト、生成コード、エラーレポート |
その帰結として、本番環境で見つかったバグには、そのバグが存在したレベルのユニットテストを追加します。たまたまそのバグをカバーするエンドツーエンドテストではありません。
コピー&ペーストよりテーブル駆動
1つのテスト、ケースのテーブル、1つのループで記述します。
#[test]
fn parses_durations() {
let cases = [
("1s", Some(Duration::from_secs(1))),
("500ms", Some(Duration::from_millis(500))),
("", None),
("1x", None),
];
for (input, expected) in cases {
assert_eq!(parse_duration(input), expected, "input: {input:?}");
}
}
テーブルの効果を高めるのはメッセージです。"input: {input:?}" がなければ、3番目のケースの失敗は行番号しか報告せず、どのケースが失敗したかはわかりません。ケースごとに固有のテスト名と失敗行が必要な場合は、rstest で同じテーブルからケースごとのテストを展開します。
失敗も含めて動作を検証する
成功パスだけをカバーするテストスイートはエラー時の動作を何も示さないため、エラー型がどのように変わってもテストに通ってしまいます。呼び出し元がマッチするエラーのバリアントを検証してください。Display 文字列ではありません。文字列は契約ではありません。どのバリアントが存在するかについては /rust-errors を参照してください。
期待値の計算に、テスト対象と同じロジックを使うテストは何も検証していません。見落としやすい問題です。一見すると十分なテストに見えます――入力をループしてテスト対象の関数で期待値を組み立てたり、同じ計算式から定数を求めたりするからです。期待値は人が記述するか、参照実装、公表されたテストベクトル、手計算した例など、独立した情報源から取得してください。
決定性はマシンではなくテストの特性
すべてのジェネレーターに明示的なシードを設定し、時計を直接読み取るのではなく時刻と乱数を注入してください。また、ユニットテストや統合テストでネットワークに触れないでください。不安定なテストは、不安定さが現れた日に削除するか修正します。不安定なテストを1つでも許容するテストスイートは、読み直す代わりに再実行することを皆に教えてしまいます。
カバレッジは指標であり、目標ではない
cargo llvm-cov はテストする価値のある分岐を見つけるのに役立ちます。パーセンテージによるゲートは、検証済みの動作ではなく、行に触れるだけのテストを生み出します。数値を追いかけないでください。
移植版の差分テスト
Rust 実装で既存の実装を置き換える場合、最も強力な証拠は、同じ入力で両方を実行して結果が一致することです。3 種類のハーネス(記録済みコーパス、並列実行、共通ジェネレーターを使った proptest)とそれぞれのトレードオフ、差分の正規化と記録のルールについては、DIFFERENTIAL-TESTING.md を参照してください。許容される差分を定めるパリティ契約は /port-to-rust の担当です。このスキルが提供するのは仕組みであり、契約ではありません。
先送りする項目
エラーのバリアント設計については /rust-errors を参照してください。unsafe コードのテストには Miri を使います。詳しくは /unsafe-rust を参照してください。非同期テストの種類、時間の制御、実時間のスリープを使う #[tokio::test] がなぜ不安定なテストを生みやすいのかについては /async-rust を参照してください。インターリーブを期待するのではなく探索する並行性テストには loom を使います。詳しくは /rust-concurrency を参照してください。本当はベンチマークであるテストには criterion を使います。詳しくは /rust-performance を参照してください。レビューの判定として、変更にテストがそもそも必要かどうかについては /rust-code-review を参照してください。
検証
cargo test --all-features
cargo test --doc
cargo clippy --all-targets --all-features # add -- -D warnings only if the repo has no lint config
Clippy は、リポジトリで設定されている lint レベルで実行します。clippy.toml や独自の lint 属性があるリポジトリでは、それらを維持し、より厳しいレベルを上乗せすることはありません。
新しいテストは、採用する前に失敗することを確認する必要があります。修正をコメントアウトするか、アサーションを反転させて、失敗メッセージが実際の問題を示していることを確認してください。一度も失敗を確認していないテストは、何もアサートしていない可能性があります。