rust-testing-code-review について
Rust のテストコードをレビューするスキルです。テスト構成、非同期処理、アサーション、分離性、カバレッジを確認し、edition に応じた検査と検証を経て指摘をまとめます。
主な機能
- 01Rust edition に応じたレビュー項目
- 02単体テストと統合テストの構成
- 03非同期テストとテストダブルの指針
- 04アサーション、分離性、エラー経路の確認
- 05並行処理と高度なテストのカバレッジ確認
活用シーン
- 01Rust crate のテスト構成レビュー
- 02非同期テストとテストダブルの確認
- 03境界ケースとエラー経路のカバレッジ評価
- 04並行処理のテスト手法のレビュー
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ existential-birds/ beagle --skill rust-testing-code-reviewインストール後、Claude Code で/rust-testing-code-reviewを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/advanced-testing.mdconcurrency-testing.mdintegration-tests.mdunit-tests.md詳細
Rust テストのコードレビュー
レビューワークフロー
- Rust エディションを確認する —
Cargo.tomlに記載されたエディションを確認します(2021 と 2024)。エディション 2024 では、if letと末尾の式における一時値のスコープが変更され、リンターの抑制には#[expect]が推奨されるようになります - テストの構成を確認する — 単体テストは
#[cfg(test)]モジュールに、統合テストはtests/ディレクトリに配置します - 非同期テストのセットアップを確認する — 非同期テストには
#[tokio::test]を使用し、ランタイムを適切に設定します。ネイティブのトレイト内async fnを使用できるモックにasync-traitが使われていないか確認します - アサーションを確認する — 意味のあるメッセージと、適切なアサーションの種類を使用します。エディション 2024 で一時値のスコープが変更されることを踏まえて、
if letアサーションを確認します - テストの分離性を確認する — テスト間で可変状態を共有せず、セットアップとティアダウンを適切に行います。共有フィクスチャには
lazy_static!/once_cellよりもLazyLockを優先します - カバレッジのパターンを確認する — エラーパスをテストし、エッジケースを網羅します
ゲート(必須)
各合格条件を満たすまで、出力形式に進まないでください(具体的な成果物を伴う yes/no で判定)。
- エディションを記録済み — 対象クレートの
Cargo.toml(またはワークスペースの[workspace.package]/継承されたエディション)を開き、editionの値を記録します。合格: Rust 2024 固有の動作(if let/末尾の一時値のドロップ、#[expect]と#[allow]の移行、トレイト内のネイティブなasync fnをデフォルトで使うこと)に言及する前に、edition = "…"と引用できるか、「ワークスペースから継承」と記録できていること。エディションが2024でない場合、これらの項目をエディション 2024 のリグレッションとして報告してはいけません。有用な場合でも、情報提供にとどめてください。 dynと静的な非同期モックの比較 —async-traitの代わりにトレイト内のネイティブなasync fnを提案する前に、モックがdyn Traitとして使われているか確認します。合格:dynが必要な場合、その提案を見送るか、有効なパターンに従い(async-traitが引き続き必要)、内容を合わせていること。- 検証プロトコル — 合格: 指摘事項を挙げる前に、review-verification-protocol スキルの手順を完了していること(提出前の指摘事項を参照)。
出力形式
次の形式で指摘事項を報告します。
[FILE:LINE] ISSUE_TITLE
Severity: Critical | Major | Minor | Informational
Description of the issue and why it matters.
クイックリファレンス
| 問題の種類 | 参照先 |
|---|---|
単体テスト、アサーション、命名、スナップショット、rstest、ドキュメントテスト、#[expect]、LazyLock フィクスチャ、末尾式のスコープ | references/unit-tests.md |
統合テスト、非同期テスト、フィクスチャ、テストデータベース、ネイティブな async fn モック、if let の一時値スコープ | references/integration-tests.md |
ファジング、proptest、Miri、Loom の基本、モック戦略、stub/fake/mock/spy の分類、rstest マトリクス、paste!、build.rs のテスト生成、criterion のベースラインと black_box の使用方針、trybuild UI テスト、clippy のリンターグループ | references/advanced-testing.md |
| Loom のインターリーブテスト、Miri による UB チェック、shuttle、ThreadSanitizer、並行コード向け CI マトリクス | references/concurrency-testing.md |
レビューチェックリスト
テスト構造
- ソースファイル内の
#[cfg(test)] mod testsに単体テストを配置する - 統合テストは
tests/ディレクトリに配置する(モジュールまたは機能ごとに 1 ファイル) - テストモジュールから親モジュールの項目にアクセスするため、
use super::*を使用する - テスト関数名でシナリオを表す:
test_<function>_<scenario>_<expected> - テストは独立しており、実行順序に依存しない
非同期テスト
- 非同期テスト関数に
#[tokio::test]を使用する - マルチスレッドの動作をテストする場合は
#[tokio::test(flavor = "multi_thread")]を使用する - 非同期テスト内で
block_onを使用しない(直接.awaitを使用する) - ハングする可能性のあるテストにタイムアウトを設定する
- モックのトレイトでは
async-traitクレートの代わりにネイティブなasync fnを使用する(Rust 1.75 から安定版)
アサーション
- 値の比較には
assert_eq!/assert_ne!を使用します(assert!よりエラーメッセージが分かりやすくなります) - 自明でないアサーションにはカスタムメッセージを付けます
- enum のバリアント確認には
matches!マクロを使用します - エラー型は文字列比較ではなく、
matches!またはパターンマッチングで確認します - 実用上可能な場合は、1 つのテストにつきアサーションを 1 つにします(失敗の診断が容易になります)
-
if letを使ったアサーションでは、エディション 2024 の一時値のスコープを確認します。条件内の一時値は早くドロップされるため、借用が無効になる可能性があります - 末尾式の戻り値について、エディション 2024 の動作を確認します。末尾式内の一時値はローカル変数より先にドロップされます
モックとテストダブル
- 依存性注入の境界としてトレイトを使用します(具象型ではなく)
- モック実装は最小限に保ちます。テストに必要なものだけを実装します
- 所有していない型はモックしません(外部依存関係は独自のトレイトでラップします)
- テストフィクスチャにはグローバル状態ではなく、ヘルパー関数を使用します
- 共有テストフィクスチャには
lazy_static!やonce_cellではなくstd::sync::LazyLockを使用します(Rust 1.80 から安定版)
エラーパステスト
- 正常系だけでなく、
Result::Errのバリアントもテストします - 特定のエラーバリアントを確認します(単に「エラーである」ことだけを確認するのではなく)
-
#[should_panic]は必要最小限にし、Resultを返すテストを優先します
テストでの lint 抑制
- テスト固有の抑制には
#[allow(lint)]ではなく#[expect(lint)]を使用します(Rust 1.81 から安定版) - テストコードのすべての
#[expect]または#[allow]に理由のコメントを付けます - 自動的に不要になるよう、古い
#[allow]属性を#[expect]に移行します
テストの命名
- テスト名は動作を説明する文のようにします(
test_happy_pathのような名前ではなく) - 関連するテストは、整理しやすいようにネストした
modブロックにまとめます - テスト名は次のパターンに従います:
<function>_should_<behavior>_when_<condition>
スナップショットテスト
- 複雑な構造化出力(JSON、YAML、HTML、CLI 出力)には
cargo instaを使用します - スナップショットは小さく、対象を絞ったものにします(巨大なオブジェクトは避けます)
- 変動するフィールド(タイムスタンプ、UUID など)にはリダクションを使用します
- スナップショットは
snapshots/ディレクトリにコミットします - 単純な値にはスナップショットではなく
assert_eq!を使用します
パラメーター化テスト
- 類似する入力に対するテスト関数の重複を避けるため、
rstestを使用します - 説明的なパラメーター化テストには、
#[case::name]属性とともに#[rstest]を使用します - 複数のテストで同じ構築が必要な場合は、共通のテストセットアップに
#[fixture]を使用します - パラメーター化テストでは、説明的なケース名を付けます(
#[case(1)]のような名前だけにしないでください) - 非同期テストと組み合わせる場合は、非同期のパラメーター化テストに
#[rstest] #[tokio::test]を使用します
ドキュメントテスト
- 公開 API 関数には、実行可能なコードを含む
/// # Examplesを付けます - ドキュメントテストは、ドキュメントと正しさの検証の両方の役割を果たします
- 例を読みやすく保つため、セットアップ用の非表示行には
#を付けます -
cargo test --docが成功します(nextest はドキュメントテストを実行しません)
並行性テスト
詳細なガイダンス:references/concurrency-testing.md
- 手書きの atomic/
unsafe impl Send|Sync/ステートマシンには、std::syncではなくloom::syncとloom::threadのシムを使った#[cfg(loom)]テストを用意します - atomic、ポインター、または
UnsafeCellに触れるunsafeを含むクレートでは、すべての PR に対して CI でcargo +nightly miri test --all-featuresを実行します(リリース時のみの実行や、モジュールレベルのcfg_attr(miri, ignore)は不可) - ロックフリーのデータ構造(
AtomicPtrを使うスタック/キュー/リスト)には、loom と Miri(Stacked Borrows および Tree Borrows)に加え、proptestによる操作シーケンスを用意します - 非決定的な入力(
Instant::now、rand、環境、HashMapの反復)はloom::modelの本体で使用しません - Loom ジョブは
LOOM_MAX_PREEMPTIONSに上限を設定し、--releaseで実行します。loom テストは別のtests/ファイルに置き、--cfg loomが通常のcargo testに影響しないようにします -
nextest run -j1を競合状態の検出範囲を示す証拠として挙げません。FFI を多用するunsafe extern "C"の経路には、ThreadSanitizer ジョブを用意します
テストの拡張(フェイク、モック、スタブ、スパイ)
詳細なガイダンス:references/advanced-testing.md
- テストダブルは目的に応じて型を使い分けています:スタブ(あらかじめ用意したデータ)、フェイク(動作するが簡略化された実装)、モック(事前に定義した期待値と検証)、スパイ(後から確認できるように呼び出しを記録)— 用語を正確に使っています
- フェイクの方がシンプルな場合に、
mockall形式のモックを使っていません(リポジトリ形式のトレイトでは、期待値の設定が多いモックよりインメモリ DB が適しています) - フェイクは本番実装と同じコントラクトに対してテストしています(トレイト適合テスト)。乖離がありません
- テストダブルを注入するためにトレイトを境界として使うパターンを採用しています — 本番コードは具象型ではなくトレイトに依存します
パフォーマンステスト(Criterion)
詳細なガイダンス:references/advanced-testing.md
- 定数畳み込みを防ぐため、ベンチマークでは
criterion::black_box(...)を使っています。ポインター形式の入力ではblack_box(&input)ではなくblack_box(input.as_ptr())を使っています - ベンチマークでは I/O を分離しています — セットアップ(ファイルを開く、メモリを割り当てる)は
iter_batchedのセットアップクロージャー内に置き、計測対象のクロージャー内には置きません - CI では
cargo bench -- --save-baseline mainでベースラインを保存し、フィーチャーブランチでは--baseline mainと比較します。回帰のしきい値を定義し、適用しています - Criterion のベンチマークは
--profile benchでビルドします(リリース最適化と、フレームグラフとの照合に使うデバッグシンボルを有効化) -
#[bench](不安定で非推奨)は使わず、criterionのみを使います - 単発の計測時間を根拠として引用していません。レポートには Criterion の統計エンジンによる平均値と分散を示しています
テスト生成
詳細なガイダンス:references/advanced-testing.md
- テーブル駆動の入力には
#[case::name(...)]を使うrstestを使用しています(テスト出力でわかりやすいケース名を表示) - 複数の軸を持つ入力マトリックスには、手作業で展開したテスト関数ではなく、複数のパラメーターで
#[values(...)]を使うrstestを使用しています(直積) - 大規模なコーパスで入力ごとに名前を付けたテストを作るには、
paste!マクロまたはbuild.rsによるコード生成を使っています(ファイル/ケースごとに#[test] fnを1つ生成) - 生成されるテストには、CI ログの差分でも変わらない安定した名前を付けています
Proc マクロの UI テスト(trybuild)
詳細なガイダンス:references/advanced-testing.md
- コンパイルエラーを出力する Proc マクロには、各失敗パスを
.stderrの参照ファイルで網羅するtrybuild::TestCasesがあります - 最新の rustc 更新後に
.stderrの出力を再生成しています(そうしないと、CI で誤検知による失敗が発生します)。TRYBUILD=overwrite cargo testを CONTRIBUTING に記載しています - trybuild のジョブでは、CI で特定の stable rustc を固定しています(
.stderrの形式は stable リリース間で変わります) - trybuild のテストは nightly ではスキップします(nightly の診断は stable と異なります)
Clippy lint グループの方針
詳細なガイダンス:references/advanced-testing.md
-
clippy::correctnessはdenyにしています(常に適用します。これらはバグです) - ライブラリでは
clippy::suspiciousをwarnまたはdenyにしています -
clippy::perfをwarnにしています(ホットパスで実際に効果があります) - ライブラリでは
clippy::pedanticを有効にしています。抑制には、単独の#[allow]ではなく、理由を示した#[expect(clippy::lint_name, reason = "...")]を使います - CI では
clippy::nurseryを有効にしていません(実験的で、rustc のリリースごとに変更されます) -
clippy::restrictionlint は個別にオプトインします。グループ全体を有効にすることはありません
重大度の基準
重大
- テストは成功するものの、実際には動作を検証していない(誤った値に対するアサーション)
- テスト間で可変状態を共有しているため、結果が不安定になる
- セキュリティ上重要なコードのエラーパスのテストがない
重大
expectedメッセージのない#[should_panic](誤ったパニックを含む、あらゆるパニックを捕捉します)- テストのセットアップで使われ、実際の失敗箇所を分かりにくくする
unwrap() - 実行順序に依存するテスト
- エディション 2024 の一時値のスコープ規則により問題が発生する、アサーション内の一時値を使ったインラインの
if let - プロジェクトがエディション 2024 を対象としており、トレイトでネイティブの
async fnが利用可能な場合に、モックトレイトで使用されるasync-trait
軽微
- 複雑な比較でアサーションメッセージがない
assert_eq!(x, y)ではなくassert!(x == y)を使っている(エラーメッセージが分かりにくくなります)- シナリオを説明していないテスト名
- ヘルパーに抽出できる冗長なセットアップコード
- 自動で解消される抑制を実現できる
#[expect]ではなく#[allow]を使用している LazyLockが利用可能なのに、テスト用フィクスチャでlazy_static!またはonce_cellを使用している
参考情報
proptestまたはquickcheckを使ったプロパティベーステストの追加提案- 複雑な出力に対するスナップショットテストの追加提案
- カバレッジを改善できる機会
有効なパターン(指摘しないでください)
- テスト内の
unwrap()/expect()— 予期しないエラーでパニックするのは、テストとして正しい動作です - テストモジュール内の
use super::*— 親の項目にアクセスする標準的なパターンです - テストヘルパーに付ける
#[allow(dead_code)]— ヘルパー関数はすべてのテストで使われるとは限りません - テスト内の
clone()— パフォーマンスより明瞭さを優先します - 大きなテスト関数 — 統合テストは長くなることがあり、ヘルパーへの抽出で常に明瞭になるとは限りません
- ブール値のチェックに使う
assert!— 式が明らかにブール値である場合(.is_some()、.is_empty())は問題ありません - 1つの論理的な動作を検証する複数のアサーション — 1つの動作に複数のチェックが必要な場合があります
Resultを返すテスト関数でのunwrap()—?で伝播させる方法も問題ありませんが、必須ではありませんdynディスパッチが必要なモックトレイトでのasync-trait— トレイトのネイティブなasync fnはdyn Traitをサポートしないため、この場合は引き続きasync-traitが必要です- テストヘルパーの正当な理由を添えた
#[expect]— 自動で解消される lint 抑制は、テストコードとして適切です - 高コストな共有テスト用フィクスチャでの
LazyLock— スレッドセーフな遅延初期化は、テスト用グローバル変数に適しています
指摘を報告する前に
問題を報告する前に、review-verification-protocol スキルを読み込み、その指示に従ってください。
コメント
Markdown 対応