測試
Rust 內建測試框架,不需要額外安裝套件。用 #[test] 標記函式,再執行 cargo test 就會自動找出並執行所有測試。編譯器已經幫你擋掉許多錯誤,測試則負責驗證「邏輯是否正確」。
單元測試
慣例是把測試寫在同一個檔案的最下方,放進 tests 模組,並用 #[cfg(test)] 標記:這個模組只有在 cargo test 時才會被編譯,不會進入正式的執行檔。
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn divide(a: i32, b: i32) -> Option<i32> {
if b == 0 { None } else { Some(a / b) }
}
#[cfg(test)]
mod tests {
use super::*; // 引入外層模組的所有項目
#[test]
fn add_works() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn divide_by_zero_returns_none() {
assert_eq!(divide(10, 0), None);
}
#[test]
fn divide_normally() {
assert_eq!(divide(10, 2), Some(5));
}
}
執行:
輸出會列出每個測試通過(ok)或失敗(FAILED)。
常用的斷言巨集
| 巨集 | 用途 |
|---|---|
assert!(條件) |
條件必須為真 |
assert_eq!(左, 右) |
兩邊必須相等,失敗時會印出兩邊的值 |
assert_ne!(左, 右) |
兩邊必須不相等 |
這些巨集都可以附上自訂訊息:assert!(x > 0, "x 應該是正數,實際是 {x}")。
預期會 panic 的測試
用 #[should_panic] 驗證「在錯誤情況下確實會失敗」:
pub fn get_item(items: &[i32], index: usize) -> i32 {
if index >= items.len() {
panic!("索引 {index} 超出範圍");
}
items[index]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "超出範圍")]
fn out_of_range_panics() {
get_item(&[1, 2, 3], 10);
}
}
回傳 Result 的測試
測試函式也可以回傳 Result,這樣就能在裡面使用 ?:
#[cfg(test)]
mod tests {
#[test]
fn parse_works() -> Result<(), std::num::ParseIntError> {
let n: i32 = "42".parse()?;
assert_eq!(n, 42);
Ok(())
}
}
整合測試與文件測試
- 整合測試:放在專案根目錄的
tests/資料夾(與src/同層),每個檔案是獨立的 crate,只能使用你公開(pub)的介面,用來驗證各部分能否一起運作。要使用整合測試,專案必須是函式庫 crate(有src/lib.rs)。 - 文件測試:寫在文件註解
///中的程式碼區塊也會被cargo test執行,確保文件裡的範例不會過期。
/// 把兩個數字相加。
///
/// ```
/// let sum = my_crate::add(2, 3);
/// assert_eq!(sum, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
控制執行哪些測試
cargo test add # 只執行名稱含 "add" 的測試
cargo test -- --nocapture # 顯示測試中 println! 的輸出
cargo test -- --ignored # 執行被 #[ignore] 標記的耗時測試
測試預設會以多執行緒並行執行,因此測試之間不應依賴共同的外部狀態(例如同一個檔案)。
實用心法
- 每修一個 bug,就先寫一個能重現它的測試,避免日後復發。
- 測試名稱要能說明「預期的行為」,例如
divide_by_zero_returns_none。 - 測試放得離程式碼近,寫程式時養成順手補一個測試的習慣。
推薦影音
Rust 的測試
簡述:Let's Get Rusty 依照官方書籍第 11 章製作,介紹 #[test]、斷言巨集與 cargo test 的用法。
下一頁:智慧指標。