技能:dedup-rust-tests
此技能中的檔案參考 (@path) 是相對於此目錄的。
我的功能
我有兩種協同工作的模式:
-
DEDUP(重複檢測) — 識別並移除冗餘的 Rust 單元測試,這些測試與其他測試覆蓋相同的程式碼路徑,並使用相同或等效的輸入值。通過
cargo llvm-cov覆蓋率比較(前後對比)進行驗證。 -
GAP-FILL(缺口填補) — 在去重後,分析
llvm-cov報告以查找未測試或測試不足的程式碼(特定的泛型具體化、trait 實現、功能門控程式碼),然後編寫針對性的單元測試來填補這些缺口。通過覆蓋率改善進行驗證。
兩種模式共享一個共同的基準構建和覆蓋率測量工作流程。
何時使用我
用於 DEDUP(重複檢測):
- 你的 Rust 測試套件有機增長,多個測試模組覆蓋相同的程式碼路徑
- 一個「舊式」內聯測試模組 (
#[cfg(test)] mod tests) 與較新的獨立測試檔案 (more_tests.rs) 共存,後者涵蓋了舊的測試 - 一個「煙霧測試」(
it_works) 複製了多個專注測試中的斷言 - 你接手了一個包含重複測試場景的程式碼庫(例如,
test_abs+test_fraction_abs都在測試相同的方法和重疊的輸入) - 你想在不損失覆蓋率的情況下縮短 CI 測試時間
用於 GAP-FILL(缺口填補):
- 泛型類型僅針對一種具體類型進行了測試(例如,
Fraction<i32>)——覆蓋率報告揭示了未測試的具體化(Fraction<i8>、Fraction<i128>) - Trait 實現(
From<i8>、TryFrom<...>)顯示 0% 的覆蓋率 - 功能門控程式碼 (
#[cfg(feature = "...")]) 未經測試 - 私有方法(
signum()、is_positive())未被覆蓋,因為它們只能從它們自己的模組內部進行測試
請勿在以下情況使用我:
- 測試是基於屬性的 / 生成式(quickcheck、proptest)測試不變量——這些重複是有意為之
- 該測試是唯一執行特定程式碼路徑的測試,即使輸入值看起來相似
- 該專案沒有配置程式碼覆蓋率工具
- 你只需要添加測試,而無需先檢查重複性
我的工作方式
第零階段:了解測試環境
# 列出所有 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 屬性測試——永遠不要移除它們
- 文檔測試 (`/// ``` ... ````)——也有助於覆蓋率
關鍵問題:
- 是否存在多個覆蓋相同程式碼的測試模組?(例如,
fractions.rs::testsvsmore_tests.rs) - 「舊式」測試是否與「全面」測試共存?
- 是否存在一個煙霧測試(
it_works),它複製了專注測試中的斷言? - 使用了哪些泛型類型參數?
第一階段:檢測重複
等級 1:相同斷言與相同值
如果兩個測試使用完全相同或嚴格子集的輸入值測試同一個函數,則它們是重複的:
// 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:相同程式碼路徑,等效值
如果兩個測試使用等效輸入測試同一函數路徑,則它們是重複的:
// 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(保留——覆蓋了乘/除,也覆蓋了加/減/負/倒數)
#[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:跨模組重複測試
同一個函數在兩個單獨的測試模組中進行了測試,且場景重疊:
// 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 測試——它們使用隨機輸入執行並驗證不變量
- 特定類型唯一的測試——即使看起來相似,它測試的是不同的泛型具體化
- 程式碼路徑的唯一執行測試——檢查覆蓋率報告
第二階段:建立基準線(移除前)
# 生成基準覆蓋率
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"
第三階段:移除重複測試
使用精確編輯從其檔案中移除每個重複的測試函數。每次移除批次後,執行:
cargo test --all-features --workspace
如果編譯因未使用的導入而失敗(例如,use num_integer::gcd; 僅被移除的測試使用),請清理它們。
常見清理任務:
- 移除僅用於已移除測試的
#[cfg(test)] use ...;導入 - 如果
use num_integer::gcd;僅被已移除的煙霧測試使用,則移除模組級別的導入 - 移除引用已移除函數的註解掉的測試程式碼
第四階段:重新構建並收集去重後指標
cargo llvm-cov --all-features --workspace --summary-only
cargo llvm-cov --all-features --workspace --lcov --output-path coverage-after-dedup.lcov
DEDUP 模式的驗證標準:
| 檢查項 | 要求 |
|---|---|
| 所有測試通過 | 必須通過 |
| 每個檔案的行覆蓋率 | 不得下降(上升是可以的——二進位制檔案中測試行數減少 = 更高的覆蓋率比例) |
| 遺漏區域計數 | 應下降或保持不變(移除死測試程式碼可能會提高覆蓋率比例) |
| 沒有新的未覆蓋程式碼路徑 | 比較前後 lcov 中的 FNDA:0 條目 |
如果任何檔案的覆蓋率下降,立即恢復——該測試並非重複。
第五階段:分析覆蓋率缺口(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_ # From<i8> for Fraction — 未測試
Rust 中常見的缺口模式:
| 模式 | 範例 | 缺少什麼 |
|---|---|---|
| 未測試的泛型具體化 | Fraction<i8> 從未被實例化 | 使用 Fraction::<i8>::new(...) 添加測試 |
| 未測試的 trait 實現 | From<i8> for Fraction<i32> | 添加 Fraction::from(5i8) 測試 |
| 未測試的 TryFrom 實現 | TryFrom<Fraction<i64>> for Fraction<i32> | 添加 Fraction::<i32>::try_from(f_i64) 測試 |
| 私有方法 | signum()、is_positive() 對於 i8 未覆蓋 | 必須從同一 #[cfg(test)] mod tests 內部測試 |
| 功能門控程式碼 | #[cfg(feature = "serde")] 未測試 | 使用 --features serde 添加測試 |
| 特定類型的 Neg | Neg 實現對於 Fraction<i8> | 添加 -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——檢查是否覆蓋(通常是通過From<i32> for Fraction<i64>)i8——很少覆蓋(需要顯式測試)i16——很少覆蓋i128——可能通過 quickcheck 覆蓋BigInt——通過功能門控測試覆蓋
第六階段:編寫針對性測試
對於未測試的泛型具體化(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));
}
對於未測試的 trait 實現
#[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 - 不要從單獨的測試檔案測試私有方法——它們會編譯失敗
第七階段:重新構建並收集填補缺口後指標
cargo test --all-features --workspace
cargo clippy --all-targets --all-features --workspace
cargo llvm-cov --all-features --workspace --summary-only
第八階段:報告覆蓋率改善
| 指標 | 之前 | 之後 | 差值 |
|-----------------------|----------|----------|----------|
| 測試計數 | 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<...
…
[為翻譯而截斷]