Communityコーディング&開発github.com

luk036/rust-重複テスト削除

Rustの重複した単体テストを検出・削除し、カバレッジのギャップを対象を絞ったテストで埋めます。すべての操作はcargo llvm-covを用いたbefore/after比較で検証されます。

rust-重複テスト削除 とは?

このスキルは、Rustコードベースにおける重複した単体テストを特定・削除し、同時にカバレッジのギャップを埋めるための支援を行います。開発者が手動でテストの重複を見つけるのは困難なため、スキルはまず既存のテスト構造を解析し、同じコードパスを異なる値でカバーするテストや、スモークテストが個別のテストに包含されているケースを検出します。削除後はcargo llvm-covでカバレッジが低下していないことを確認し、その後カバレッジレポートで未テストの単相化(例:Fraction<i8>)、トレイト実装(From<i8>)、プライベートメソッド(signum())、フィーチャーゲートされたコードを特定します。これらのギャップに対しては、対象を絞ったテストを適切な場所(プライベートメソッドは同一モジュール内、トレイト実装は専用テストファイル内)に追加します。このスキルはClaude CodeやCursorなどのAIエージェントで使用されることを想定しており、CI時間の短縮とカバレッジ向上を両立することを目的としています。クイックチェックやプロップテストなど、意図的に冗長なテストは削除しません。

対応~Claude Code~Codex CLI~Cursor✓OpenCode
npx skills add https://github.com/luk036/agent-skills/tree/HEAD/skills/dedup-rust-tests

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

スキル: dedup-rust-tests

このスキル内のファイル参照(@path)は、このディレクトリからの相対パスです。

私の役割

私は連携して動作する2つのモードを持っています。

  1. DEDUP(重複検出・削除) — 別のテストが同じコードパスを同一または同等の入力値で既にカバーしている、Rustの単体テストを特定・削除します。cargo llvm-cov のカバレッジ比較(before vs after)で検証します。

  2. GAP-FILL(カバレッジギャップ埋め) — DEDUPの後、llvm-cov レポートを分析して未テストまたはテストが不十分なコード(特定の単相化、トレイト実装、フィーチャーゲートされたコード)を特定し、それらのギャップを埋めるための対象を絞ったテストを作成します。カバレッジの改善で検証します。

両モードとも共通のベースライン構築とカバレッジ測定のワークフローを共有します。

使用するタイミング

DEDUP用:

  • Rustのテストスイートが有機的に成長し、複数のテストモジュールが同じコードパスをカバーしている場合
  • 「旧式」のインラインテストモジュール(#[cfg(test)] mod tests)が、新しい別のテストファイル(more_tests.rs)と共存し、後者が前者を包含している場合
  • 「スモークテスト」(it_works)が複数の焦点を絞ったテストのアサーションを重複している場合
  • 重複したテストシナリオを含むコードベースを引き継いだ場合(例:test_abs + test_fraction_abs が同じメソッドを重複する入力でテスト)
  • CIテスト時間をカバレッジを失わずに短縮したい場合

GAP-FILL用:

  • ジェネリック型が1つの具象型でのみテストされている場合(例:Fraction<i32>)— カバレッジレポートが未テストの単相化(Fraction<i8>, Fraction<i128>)を明らかにする場合
  • トレイト実装(From<i8>, TryFrom<...>)のカバレッジが0%の場合
  • フィーチャーゲートされたコード(#[cfg(feature = "...")])が未テストの場合
  • プライベートメソッド(signum(), is_positive())が自身のモジュール内からのみテスト可能なためにカバレッジがない場合

使用すべきでないケース:

  • テストがプロパティベース/生成テスト(quickcheck、proptest)で不変条件をテストしている場合 — これらは設計上意図的に冗長です
  • テストが特定のコードパスを唯一実行するものである場合(値が似ていても)
  • プロジェクトにコードカバレッジツールが設定されていない場合
  • 冗長性を最初に確認せずにテストを追加するだけの場合

動作の仕組み

フェーズ0: テスト環境の理解

# すべてのRustソースファイルを一覧表示
Get-ChildItem -Recurse -Filter *.rs

# すべてのテストアノテーションとモジュールを検索
Select-String -Path "src/**/*.rs" -Pattern "#\[test\]|#\[cfg\(test\)\]|#\[quickcheck\]|mod tests"

# ファイルごとのテスト数をカウント
Select-String -Path "src/**/*.rs" -Pattern "^\\s*fn (test_|check_)" | Group-Object -Property Filename

テスト構造をマッピングします:

  • ソースファイル内のインライン #[cfg(test)] mod tests { ... } — プライベートメソッドにアクセス可能
  • スタンドアロンの tests/*.rs 統合テストファイル
  • フィーチャーゲートされたテストモジュール(#[cfg(feature = "...")])
  • Quickcheck/proptest プロパティテスト — これらは決して削除しない
  • Doc-tests(`/// ``` ... ````)— これもカバレッジに寄与します

重要な質問:

  • 同じコードをカバーする複数のテストモジュールがありますか?(例:fractions.rs::tests と more_tests.rs)
  • 「旧式」のテストと「包括的な」テストが共存していますか?
  • 個別のテストのアサーションを重複するスモークテスト(it_works)がありますか?
  • どのジェネリック型パラメータが実行されていますか?

フェーズ1: 冗長性の検出

レベル1: 同一値による同一アサーション

2つのテストが、同一または厳密にサブセットの入力値で同じ関数を実行する場合、冗長です:

// test_abs(冗長 — test_fraction_absのサブセット)
#[test]
fn test_abs() {
    assert_eq!(Fraction::new(-3, 4).abs(), Fraction::new(3, 4));  // 下記と同じ
}

// test_fraction_abs(保持 — より多くをカバー)
#[test]
fn test_fraction_abs() {
    assert_eq!(Fraction::new(-3, 4).abs(), Fraction::new(3, 4));  // 重複
    assert_eq!(Fraction::new(3, -4).abs(), Fraction::new(3, 4));  // 一意
}

ルール: テストAのアサーションがすべてテストBに含まれている場合、テストAは冗長です。Aを削除します。

レベル2: 同等の値による同じコードパス

2つのテストが、同等の入力で同じ関数パスを実行する場合、冗長です:

// test_mul_div(冗長 — test_fraction_arithmeticに包含される)
#[test]
fn test_mul_div() {
    assert_eq!(Fraction::new(3, 4) * Fraction::new(5, 6), Fraction::new(5, 8));
    assert_eq!(Fraction::new(3, 4) / Fraction::new(5, 6), Fraction::new(9, 10));
}

// test_fraction_arithmetic(保持 — mul/divだけでなくadd/sub/neg/reciprocalもカバー)
#[test]
fn test_fraction_arithmetic() {
    assert_eq!(Fraction::new(1, 2) + Fraction::new(1, 3), Fraction::new(5, 6));
    assert_eq!(Fraction::new(1, 2) - Fraction::new(1, 3), Fraction::new(1, 6));
    assert_eq!(Fraction::new(1, 2) * Fraction::new(1, 3), Fraction::new(1, 6));
    assert_eq!(Fraction::new(1, 2) / Fraction::new(1, 3), Fraction::new(3, 2));
    assert_eq!(-Fraction::new(1, 2), Fraction::new(-1, 2));
    assert_eq!(Fraction::new(1, 2).inv(), Fraction::new(2, 1));
}

ルール: テストAが操作XとYのみをカバーするが、テストBがX、Y、かつZをカバーする場合、Aを削除します。

レベル3: モジュール間でのテストの重複

同じ関数が2つの別々のテストモジュールで重複するシナリオでテストされています:

// fractions.rs::tests::test_fraction_from_i32_to_i64(冗長)
#[test]
fn test_fraction_from_i32_to_i64() {
    let frac: Fraction<i64> = Fraction::from(3_i32);
    assert_eq!(frac, Fraction::new(3, 1));
}

// more_tests.rs::test_from_i32_to_i64(保持 — 同一、新しい方を保持)
#[test]
fn test_from_i32_to_i64() {
    let f_i32 = 3;
    let f_i64 = Fraction::<i64>::from(f_i32);
    assert_eq!(f_i64, Fraction::new(3i64, 1i64));
}

ルール: インラインモジュールと別ファイルモジュールの両方にテストが存在し、それらが同一のものをテストしている場合、重複を削除します。一般的には専用のテストファイルにあるものを保持します。

レベル4: スモークテストが個別テストに包含される場合

// it_works(冗長スモークテスト — 削除)
#[test]
fn it_works() {
    assert_eq!(const_gcd(4, -6), 2);              // test_const_gcdでカバー済み
    assert_eq!(Fraction::new(30, -40), Fraction::new(-3, 4));  // test_fraction_constructionでカバー済み
    assert_eq!(Fraction::from(30), Fraction::new(30, 1));      // test_fraction_constructionでカバー済み
    assert_eq!(Fraction::<i32>::default(), Fraction::new(0, 1)); // test_fraction_constructionでカバー済み
}

ルール: 複数の個別テストのアサーションを重複するスモークテストは削除する必要があります。カバレッジに価値を追加しません。

絶対に削除しないもの

  • Quickcheck/proptestテスト — ランダム化された入力で実行され、不変条件を検証します
  • 特定の型の唯一のテスト — たとえ似ていても、異なる単相化を実行しています
  • コードパスの唯一の実行元であるテスト — カバレッジレポートを確認してください

フェーズ2: ベースラインの構築(削除前)

# ベースラインカバレッジを生成
cargo llvm-cov --all-features --workspace --summary-only

# 行レベルの比較のために詳細レポートも保存
cargo llvm-cov --all-features --workspace --lcov --output-path coverage-before.lcov

ベースラインメトリクスを記録:

  • リージョン、行、関数のカバレッジ率
  • ミスされたリージョンの数
  • 総テスト数:cargo test --all-features --workspace 2>&1 | Select-String "test result: ok. (\d+) passed"

フェーズ3: 冗長テストの削除

各冗長テスト関数をファイルから正確な編集で削除します。削除バッチごとに、以下を実行します:

cargo test --all-features --workspace

コンパイルが未使用のインポート(例:削除されたテストでのみ使用されていた use num_integer::gcd;)のために失敗した場合は、それらをクリーンアップします。

一般的なクリーンアップタスク:

  • 削除されたテストでのみ使用されていた #[cfg(test)] use ...; インポートを削除する
  • 削除されたスモークテストでのみ使用されていたモジュールレベルの use num_integer::gcd; を削除する
  • 削除された関数を参照するコメントアウトされたテストコードを削除する

フェーズ4: 再構築と削除後メトリクスの収集

cargo llvm-cov --all-features --workspace --summary-only
cargo llvm-cov --all-features --workspace --lcov --output-path coverage-after-dedup.lcov

DEDUPの判定基準:

チェック項目要件
すべてのテストが通過必須
ファイルごとの行カバレッジ低下しない(増加はOK — バイナリ内のテスト行が減るとカバレッジ比率が上がる)
ミスされたリージョン数減少または同じ(デッドテストコードの削除はカバレッジ比率を上げる可能性がある)
新しい未カバレッジコードパスなしlcovの FNDA:0 エントリをbefore/afterで比較

どのファイルでもカバレッジが低下した場合、すぐに元に戻してください — そのテストは冗長ではありませんでした。

フェーズ5: カバレッジギャップの分析(GAP-FILLモード)

lcovファイルから未カバー関数を解析

lcovファイル内の FNDA:0 行は、正確にどの単相化が未テストかを示します:

FNDA:0,_RNvMNt...INtB2_8FractionnE7new_rawB4_   # Fraction<type_n>::new_raw — 未テスト
FNDA:0,_RNvMs2_Nt...INtB5_8FractionpE11is_positiveB7_  # Fraction<type_p>::is_positive — 未テスト
FNDA:0,_RNvXs7_Nt...INtB5_8FractionlEINtNt...4FromhE4fromB7_  # Fractionに対するFrom<i8> — 未テスト

Rustにおける一般的なギャップパターン:

パターン例不足しているもの
未テストの単相化Fraction<i8> がインスタンス化されていないFraction::<i8>::new(...) を使ったテストを追加
未テストのトレイト実装Fraction<i32> に対する From<i8>Fraction::from(5i8) テストを追加
未テストのTryFrom実装Fraction<i32> に対する TryFrom<Fraction<i64>>Fraction::<i32>::try_from(f_i64) テストを追加
プライベートメソッドsignum(), is_positive() がi8で未カバレッジ同じ #[cfg(test)] mod tests 内からテストする必要がある
フィーチャーゲートされたコード#[cfg(feature = "serde")] が未テスト--features serde でテストを追加
特定の型のNegFraction<i8> の Neg 実装-Fraction::<i8>::new(...) を追加

lcovのマングル名を解読

lcov内のRustのマングル名は型パラメータを示します。注目すべき主要なサフィックス:

  • NtB5_8FractionlE... — 型 l の Fraction(一般的には i64)
  • NtB5_8FractionnE... — 型 n の Fraction(一般的には i8)
  • NtB5_8FractionxE... — 型 x の Fraction(一般的には i128)
  • NtB5_8FractionpE... — 型 p の Fraction(ビルドによっては一般的に i8)

正確なマッピングは単相化の順序に依存します。テストで実際に使用されている型と相互参照します:

  • i32 — ほぼ常にカバーされている(全体で使用)
  • i64 — カバーされているか確認(多くの場合 Fraction<i64> に対する From<i32> 経由)
  • i8 — めったにカバーされない(明示的なテストが必要)
  • i16 — めったにカバーされない
  • i128 — クイックチェック経由でカバーされる可能性あり
  • BigInt — フィーチャーゲートされたテストでカバーされる

フェーズ6: 対象を絞ったテストの作成

未テストの単相化(i8、i16など)用

構築、セッター、算術、比較、特殊値をカバーする包括的なテストを作成します:

#[test]
fn test_i8_construction() {
    let f = Fraction::new(1i8, 2i8);
    assert_eq!(f, Fraction::new(1i8, 2));

    let f_raw = Fraction::new_raw(3i8, 4i8);
    assert_eq!(*f_raw.numer(), 3i8);
    assert_eq!(*f_raw.denom(), 4i8);
}

#[test]
fn test_i8_setters() {
    let mut f: Fraction<i8> = Fraction::new(1, 2);
    f.set_zero();
    assert!(f.is_zero());
    f.set_one();
    assert!(f.is_one());
    f.set_infinite();
    assert!(f.is_infinite());
    f.set_nan();
    assert!(f.is_nan());
}

#[test]
fn test_i8_arithmetic() {
    let f1: Fraction<i8> = Fraction::new(1, 2);
    let f2: Fraction<i8> = Fraction::new(1, 3);
    assert_eq!(f1 + f2, Fraction::new(5i8, 6));
    assert_eq!(f1 - f2, Fraction::new(1i8, 6));
    assert_eq!(f1 * f2, Fraction::new(1i8, 6));
    assert_eq!(f1 / f2, Fraction::new(3i8, 2));
    assert_eq!(-f1, Fraction::new(-1i8, 2));
    assert_eq!(f1.abs(), Fraction::new(1i8, 2));
}

プライベートメソッド用(同一モジュール内に必須)

プライベートメソッド(例:signum(), is_positive(), is_negative())は、それらが定義されている同じソースファイル内の #[cfg(test)] mod tests ブロックからのみテストできます:

// fractions.rs内、#[cfg(test)] mod tests の中:
#[test]
fn test_i8_signum_and_predicates() {
    let pos: Fraction<i8> = Fraction::new(3, 4);
    assert!(pos.is_positive());
    assert!(!pos.is_negative());
    assert_eq!(pos.signum(), Fraction::new(1i8, 1));

    let neg: Fraction<i8> = Fraction::new(-3, 4);
    assert!(neg.is_negative());
    assert_eq!(neg.signum(), Fraction::new(-1i8, 1));

    let zero: Fraction<i8> = Fraction::new(0, 1);
    assert_eq!(zero.signum(), Fraction::new(0i8, 1));
}

未テストのトレイト実装用

#[test]
fn test_from_i8_to_i32() {
    let f: Fraction<i32> = Fraction::from(5i8);
    assert_eq!(f, Fraction::new(5i32, 1));
}

#[test]
fn test_try_from_i64_to_i32() {
    use core::convert::TryFrom;
    let f_i64 = Fraction::new(5i64, 1i64);
    let f_i32 = Fraction::<i32>::try_from(f_i64).unwrap();
    assert_eq!(f_i32, Fraction::new(5i32, 1));
}

テスト値のルール

  • 狭い型には小さく安全な値を使用(例:i8 の最大値は127 — オーバーフローを避ける)
  • 正の値と負の値の両方をテストする
  • エッジケースをテストする:ゼロ分母(= 無限大)、ゼロ/ゼロ(= NaN)、符号が反対の分母
  • 恒等操作をテストする:-(-x) == x、x * 1 == x、x + 0 == x
  • 別のテストファイルからプライベートメソッドをテストしない — コンパイルに失敗します

フェーズ7: 再構築とギャップ埋め後メトリクスの収集

cargo test --all-features --workspace
cargo clippy --all-targets --all-features --workspace
cargo llvm-cov --all-features --workspace --summary-only

フェーズ8: カバレッジ改善の報告

| メトリクス             | 改善前   | 改善後   | Δ        |
|-----------------------|----------|----------|----------|
| テスト数               | N        | N'       | +Δ       |
| fractions.rs リージョン | 93.76%   | 98.45%   | +4.69%   |
| fractions.rs 行       | 92.77%   | 98.97%   | +6.20%   |
| fractions.rs 関数     | 88.10%   | 98.67%   | +10.57%  |
| ミスされたリージョン数  | 70       | 15       | -55      |

削除された冗長テスト数: 11
埋められたカバレッジギャップ: Fraction<i8>, From<i8>, From<u8>, TryFrom<...

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

関連スキル