mcpField
R
rust-write-tests

rust-write-tests

github.com/pproencaテスト・品質218244 回インストール11 週間前に更新
リポジトリ

rust-write-tests について

不変条件に結び付いた回帰テストを作成する Rust 開発者向けスキルです。境界条件、決定的なアサーション、イベント駆動による不安定なテストの調査を重視します。

主な機能

  1. 01不変条件に基づくテスト設計
  2. 02境界条件とプラットフォーム差の確認
  3. 03型付きフィクスチャとオブジェクト比較
  4. 04決定的なテストデータ
  5. 05イベント駆動の不安定テスト調査
  6. 06テスト命名とレビュー項目

活用シーン

  1. 01Rust コードの回帰テスト設計
  2. 02壊れやすい文字列アサーションの置き換え
  3. 03タイミング依存の非同期テスト調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/pproenca/dot-skills --skill rust-write-tests

インストール後、Claude Code で/rust-write-testsを入力して読み込まれたか確認します。

同梱ファイルAGENTS.mdmetadata.json

詳細

Rust テスト作成スキル

実際のバグを検出できるテストを書きます。すべてのテストは、コードが「動く」ことを示すだけでなく、特定の不変条件を守る必要があります。

「何が壊れる可能性があるか?」フレームワーク

テストを書く前に、次の4つの質問に答えてください。

  1. このコードが維持する不変条件は何ですか?(例:「デシリアライズされた設定には、常にデフォルトのプロファイルがある」)
  2. どのようなエッジケースがその条件を破りますか?(例:「空の TOML テーブル、キーの欠落、未知のキーの追加」)
  3. どのようなプラットフォーム差異が表面化する可能性がありますか?(例:「パスの区切り文字、大文字と小文字の区別、シンボリックリンクの動作」)
  4. 将来のリファクタリングで、誤って何が壊れる可能性がありますか?(例:「構造体にフィールドを追加したものの、Display 実装には追加し忘れる」)

#1 にしか答えられないなら、それはハッピーパステストです。4つすべてに答えれば、回帰テストスイートになります。

5つのテスト改善

それぞれの改善では、表面的なテストパターンと、専門的な置き換え方法を示します。

改善 1:弱いアサーション → オブジェクト全体の比較
// BEFORE: proves nothing about the rest of the struct
let result = parse_config(input)?;
assert!(result.is_ok());

// AFTER: catches any unexpected field change
use pretty_assertions::assert_eq;
let result = parse_config(input)?;
assert_eq!(result, Config {
    name: "default".into(),
    timeout: Duration::from_secs(30),
    retries: 3,
    verbose: false,
});
改善 2:単一のハッピーパス → 目的を絞ったテストスイート
// BEFORE: one test, one path
#[test]
fn test_parse_config() {
    let cfg = parse("valid input").unwrap();
    assert!(cfg.is_valid());
}

// AFTER: 3-6 tests covering happy, error, edge, platform
#[test]
fn parse_config_returns_defaults_for_minimal_input() { .. }
#[test]
fn parse_config_rejects_negative_timeout() { .. }
#[test]
fn parse_config_preserves_unknown_fields_as_extensions() { .. }
#[test]
fn parse_config_handles_empty_string_gracefully() { .. }
#[cfg(windows)]
#[test]
fn parse_config_normalizes_backslash_paths() { .. }
改善 3:インラインのテストモジュール → 同階層の _tests.rs ファイル
// BEFORE: tests pollute the production file diff
// foo.rs
pub fn compute() -> u32 { 42 }
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn it_works() { assert_eq!(compute(), 42); }
}

// AFTER: production code and test code in sibling files
// foo.rs
pub fn compute() -> u32 { 42 }
#[cfg(test)]
#[path = "foo_tests.rs"]
mod tests;

// foo_tests.rs
use super::*;
#[test]
fn compute_returns_expected_value() { assert_eq!(compute(), 42); }

mod.rs モジュールには、mod_tests.rs を使用します。

改善 4:文字列ベースのテストデータ → 型付き構造体の構築
// BEFORE: silent breakage when fields change
let input: Config = serde_json::from_str(r#"{"name":"test","timeout":30}"#)?;

// AFTER: compile-time safety for field additions/renames
fn make_config(name: &str, timeout_secs: u64) -> Config {
    Config {
        name: name.to_string(),
        timeout: Duration::from_secs(timeout_secs),
        retries: 0,
        verbose: false,
    }
}
let input = make_config("test", 30);

ドメインオブジェクト用のファクトリー関数を使うと、各テストで必要なフィクスチャを正確に構築できます。共有の可変状態はありません。テスト実行時に JSON を解析する必要もありません。

改善 5:フィクスチャ内の HashMap → 決定性を保つ BTreeMap
// BEFORE: test passes 99% of the time, flakes in CI
let mut map = HashMap::new();
map.insert("b", 2);
map.insert("a", 1);
assert_eq!(format!("{map:?}"), r#"{"a": 1, "b": 2}"#); // order not guaranteed

// AFTER: deterministic iteration order
let mut map = BTreeMap::new();
map.insert("b", 2);
map.insert("a", 1);
assert_eq!(format!("{map:?}"), r#"{"a": 1, "b": 2}"#); // always this order

出力順序がアサーションやスナップショットに影響する場合は、必ず BTreeMap を使用します。

テストのフレークを特定する手順

Bolin で最も頻繁に見られるパターンです(30以上のコミットで97件以上の言及)。テストがフレークするときは、次の手順を厳守してください。

  1. 競合が発生する時間帯を特定する -- イベント発行コードを読み、タイミングの前提がどこで破綻するかを確認します。イベントが到着した、または状態が変化したという証拠がないのに、テストがそう仮定している箇所を正確に特定します。
  2. タイミング依存をイベント駆動の同期に置き換える -- スリープしたり順序を仮定したりせず、特定のイベントを待ちます。同期プリミティブとして sleep を使ってはいけません。
  3. アサーションを順序に依存しないものにする -- 収集した値をソートする、集合を使う、位置ではなく内容で照合するといった方法を使います。イベント順序が非決定的であること自体はバグではありません。それをアサーションで前提とすることがバグです。
  4. 修正をストレステストする -- 次の正確なコマンドを実行します:cargo nextest run -p <crate> -j 2 --no-fail-fast --stress-count 50 --status-level leak
  5. 非決定性をコミットメッセージに記録する -- タイミングの前提が誤っていた理由と、代わりにどのような同期方法を使ったかを説明します。
// BEFORE (timing-dependent):
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(events.len(), 2);
assert_eq!(events[0].type_name, "item.create");
assert_eq!(events[1].type_name, "audio.delta");

// AFTER (event-driven, order-independent):
wait_for_event(&rx, |e| e.type_name == "item.create").await;
wait_for_event(&rx, |e| e.type_name == "audio.delta").await;
// OR: collect, sort, compare
let mut types: Vec<_> = events.iter().map(|e| &e.type_name).collect();
types.sort();
assert_eq!(types, vec!["audio.delta", "item.create"]);

よくあるフレークの原因に注意してください。

  • 状態が実際に準備できる前に楽観的に発行される turn/started
  • 非同期チャネル(mpsc、broadcast)をまたいだイベント順序
  • シリアライズされた出力における HashMap の反復順序
  • 子プロセスのシャットダウンと競合するテストハーネスの Drop(まず stdin を閉じ、その後に待機し、最後に kill する)

意図ベースのアサーション

コマンド文字列の完全一致を、意図に基づくセマンティックな照合に置き換えます。プラットフォームやリファクタリングによって変わる特定のコマンド形式ではなく、テストが正しい INTENT(操作+対象)を検証していることを確認します。

// BEFORE: brittle -- breaks if command formatting changes
assert_eq!(cmd.to_string(), "rm -rf /tmp/workspace/build");

// AFTER: intent-based -- asserts the operation and target
assert_eq!(cmd.operation(), Operation::Remove);
assert!(cmd.target().ends_with("workspace/build"));
assert!(cmd.is_recursive());

完全一致が避けられない場合は、フォーマット済み文字列全体ではなく、意味のある部分(パスの末尾、フラグの有無)をアサートします。

テストの命名規則

パターン:{subject}_{scenario}_{expected_outcome}

失敗したテスト名は、次のアクションにつながるバグの説明でなければなりません。CI で失敗したとき、名前だけで何が壊れたかわかるようにします。テスト名は仕様として読めるものにします。失敗した場合、どの不変条件に違反したのかが正確にわかる必要があります。

優れた名前の例:

sandbox_detection_requires_keywords
sandbox_detection_ignores_non_sandbox_mode
aggregate_output_rebalances_when_stderr_is_small
parse_config_rejects_negative_timeout
permissions_profiles_reject_writes_outside_workspace_root
permissions_profiles_allow_network_enablement
legacy_sandbox_mode_config_builds_split_policies_without_drift
under_development_features_are_disabled_by_default
usage_limit_reached_error_formats_free_plan
unexpected_status_cloudflare_html_is_simplified
root_write_plus_carveouts_still_requires_platform_sandbox
explicit_unreadable_paths_prevent_auto_approval_for_external_sandbox
denied_hosts_take_priority_over_allowed_hosts_glob

アンチパターンとなる名前:test_parse、it_works、test_config_1、happy_path。

テストスタックのクイックリファレンス

シナリオツール
HTTP モックwiremock::MockServer
ファイルシステムの分離TempDir(tempfile クレート)
非同期テスト#[tokio::test]
UI/出力のスナップショットinsta::assert_snapshot!
構造体の比較pretty_assertions::assert_eq
列挙型のバリアント確認assert_matches!
決定性のあるコレクションHashMap より BTreeMap
不安定なテストのストレステストcargo nextest run --stress-count 50
それぞれの使いどころ
  • wiremock:実際の HTTP エンドポイントにアクセスするテストには、すべて使用します。Mock::given().respond_with() でレスポンスをマウントします。テスト後にリクエストボディを検証します。
  • TempDir:ディスクにアクセスするテストには、すべて使用します。プロセスの環境変数は変更しないでください。/tmp や C:\ をハードコードしないでください。
  • insta:TUI ウィジェット、CLI 出力、エラーメッセージなど、正確なテキストが重要なものに使用します。バッファーにレンダリングし、assert_snapshot! でスナップショットを取得します。
  • pretty_assertions:すべての assert_eq! 呼び出しで標準的に使用します。失敗時に色付きの差分を表示します。各テストファイルの先頭でインポートします。
  • nextest のストレステスト:不安定なテストを修正した後に実行します。並行実行下でも修正が維持されることを確認するため、必ず -j 2 --no-fail-fast --stress-count 50 を指定して実行します。

セルフレビューチェックリスト

テストを作成した後、すべての項目を確認してください。テストを提示する前に、違反をすべて修正してください。

[ ] Uses pretty_assertions::assert_eq (not std assert_eq)
[ ] Compares entire objects, not individual fields
[ ] Each test guards a specific invariant (not just "it works")
[ ] Test names follow {subject}_{scenario}_{expected_outcome}
[ ] Test names encode the invariant being guarded
[ ] TempDir for any filesystem tests (no hardcoded paths)
[ ] No process environment mutation (no std::env::set_var)
[ ] Error paths tested (not just happy path)
[ ] At least 3 tests for any non-trivial function
[ ] BTreeMap used where iteration order affects assertions
[ ] Test file is a sibling _tests.rs, not inline mod tests {}
[ ] No timing-dependent assertions (no sleep -> assert)
[ ] Order-independent where event order is non-deterministic
[ ] String assertions use intent matching, not exact format

コメント