5.1 單元測試:內建的測試框架#
Rust 測試的優勢:內建、零設定#
Rust 內建測試框架——不需要選擇 JUnit、pytest、Jest,cargo test 就是全部。測試程式碼直接寫在原始碼裡,用 #[test] 標記:
// src/lib.rs(或 src/main.rs 內也可以)
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)] // 這個模組只在測試時編譯
mod tests {
use super::*; // 引入上層的項目
#[test]
fn test_add() {
assert_eq!(add(2, 3), 4);
}
}
cargo test
# running 1 test
# test tests::test_add ... FAILED
#
# failures:
# tests::test_add
#
# assertion `left == right` failed
# left: 5
# right: 4
斷言三兄弟#
#[test]
fn test_assertions() {
assert!(1 + 1 == 2); // 布林條件
assert_eq!(add(2, 3), 5); // 相等(值要實作 Debug 與 PartialEq)
assert_ne!(add(2, 3), 4); // 不相等
assert!(v.is_empty(), "v 應該是空的,實際:{:?}", v); // 自訂失敗訊息
}
注意:
assert_eq!的值需要#[derive(Debug, PartialEq)]——忘記 derive 時編譯器會提醒你。
慣例:測試模組放哪#
Rust 的慣例是測試與被測程式碼放在同一個檔案的 #[cfg(test)] mod tests 中:
// src/lib.rs
pub fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn divide_normal() {
assert_eq!(divide(6.0, 3.0), Some(2.0));
}
#[test]
fn divide_by_zero() {
assert_eq!(divide(6.0, 0.0), None); // 測「錯誤路徑」
}
}
工程意義:
- 測試就在被測程式碼旁邊,改程式碼時順手看到測試
- 可以測試私有函式(同一個 crate 內看得見)
#[cfg(test)]保證測試程式碼不會被編進正式產物
測試錯誤路徑:Result 與 should_panic#
// 測回傳 Result 的函式:直接用 ?,失敗會讓測試失敗
#[test]
fn read_ok() -> Result<(), std::io::Error> {
let s = read_config()?;
assert!(s.contains("port"));
Ok(())
}
// 測 panic:應該崩潰的才對
#[test]
#[should_panic]
fn invalid_input() {
process(-1); // 預期 panic
}
cargo test 的常用操作#
cargo test # 跑全部測試
cargo test divide # 只跑名稱含 divide 的測試
cargo test -- --nocapture # 顯示測試中的 println! 輸出
cargo test -- --ignored # 跑標記 #[ignore] 的測試
測試是平行執行的(多執行緒)——如果你的測試共用檔案或環境變數,要注意互相干擾(或加
-- --test-threads=1)。
本章小結#
- 測試框架內建:
#[test]+cargo test,零設定 - 斷言:
assert!、assert_eq!、assert_ne!(值需 derive Debug、PartialEq) - 慣例:測試放同檔案的
#[cfg(test)] mod tests,可測私有函式 - 測 Result 直接用
?;測 panic 用#[should_panic] - 別忘了測錯誤路徑——錯誤路徑常常比成功路徑更需要測
想一想#
- 「測試與程式碼同檔案」和其他語言「tests/ 目錄」的取捨各是什麼?
- 為什麼錯誤路徑更需要測試?Rust 的 Option/Result 對此有什麼幫助?
- 測試平行執行會造成什麼問題?你的測試有哪些共用狀態?