本文 AI 產出,尚未審核
Rust 測試與文件
單元:測試屬性(#[test]、#[should_panic])
簡介
在軟體開發的全流程中,測試是保證程式品質、降低回歸風險的關鍵手段。Rust 內建的測試框架讓我們可以直接在同一個 crate 中撰寫單元測試、整合測試,並透過屬性標記 (#[test]、#[should_panic]) 來控制測試行為。
對於剛踏入 Rust 世界的開發者而言,了解這些測試屬性的語意與使用方式,能快速建立 可自動化、可維護 的測試基礎;對於已有專案的中階開發者,則能藉此提升測試的精準度與可讀性,避免因錯誤的測試寫法而產生誤判或漏測的情況。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐層深入,協助你在日常開發中熟練運用 #[test] 與 #[should_panic],並把測試當作程式碼的一部分來管理。
核心概念
1. #[test]:告訴編譯器這是一個測試函式
- 作用:在
cargo test執行時,編譯器會自動將帶有#[test]的函式收集起來,並以獨立的執行緒跑測試。 - 位置:通常放在同檔案的
mod tests模組內,並使用#[cfg(test)]只在測試編譯時包含。
範例 1:最簡單的單元測試
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// 測試模組,只在測試編譯時被包含
#[cfg(test)]
mod tests {
// 引入外層模組的內容
use super::*;
// 這裡的函式會被 cargo test 執行
#[test]
fn test_add_positive() {
assert_eq!(add(2, 3), 5);
}
}
說明:
assert_eq!失敗時會直接使測試失敗,成功則測試通過。
2. 斷言(Assertions)與測試結果
assert!,assert_eq!,assert_ne!為最常用的斷言巨集。- 若斷言失敗,測試即 失敗;若程式碼執行到結尾且未 panic,測試即 成功。
範例 2:使用多種斷言
#[cfg(test)]
mod tests {
#[test]
fn test_assertions() {
// 斷言條件為 true 時通過
assert!(true);
// 兩個值相等時通過
assert_eq!(std::mem::size_of::<u32>(), 4);
// 兩個值不相等時通過
assert_ne!(std::mem::size_of::<u64>(), 4);
}
}
3. #[should_panic]:預期程式會 panic
- 目的:測試錯誤處理路徑或保證某些不合法輸入會導致 panic。
- 使用方式:在測試函式前加上
#[should_panic],若函式 真的 panic,測試通過;若未 panic,測試失敗。
範例 3:簡易的 panic 測試
pub fn divide(dividend: i32, divisor: i32) -> i32 {
if divisor == 0 {
panic!("除數不能為零!");
}
dividend / divisor
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn test_divide_by_zero() {
// 這裡會 panic,測試預期成功
divide(10, 0);
}
}
4. #[should_panic(expected = "...")]:驗證 panic 訊息
- 為了避免測試因 其他 panic(非預期)而誤通過,可加上
expected參數,僅當 panic 訊息包含指定字串時才算通過。
範例 4:驗證 panic 訊息
#[test]
#[should_panic(expected = "除數不能為零")]
fn test_divide_by_zero_message() {
divide(5, 0); // 只要訊息包含「除數不能為零」即通過
}
5. 測試模組的結構化與可見性
mod tests常放在檔案最底部,使用use super::*;取得外層項目。- 若測試需要多個檔案,可在
tests/目錄下建立 整合測試(integration tests),這些測試不需要#[test]標記,因為每個檔案本身即為測試套件。
範例 5:跨檔案的整合測試
my_crate/
├─ src/
│ └─ lib.rs
└─ tests/
└─ integration_test.rs
tests/integration_test.rs
extern crate my_crate; // 引入 crate
#[test]
fn test_integration_add() {
assert_eq!(my_crate::add(7, 8), 15);
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記 #[cfg(test)] |
測試模組會被編譯進正式二進位,導致檔案體積變大。 | 在測試模組前加上 #[cfg(test)]。 |
測試函式未加 #[test] |
cargo test 不會執行此函式,容易誤以為測試成功。 |
確認每個測試函式都有 #[test] 標記。 |
使用 #[should_panic] 卻未指定 expected |
其他意外 panic 也會讓測試通過,降低測試可信度。 | 加上 expected = "具體訊息",或改用 Result 方式測試錯誤。 |
| 斷言過於寬鬆 | assert!(true) 永遠通過,失去測試意義。 |
使用具體的斷言,如 assert_eq!、assert_ne!,或自訂斷言巨集。 |
| 測試依賴全域狀態 | 多個測試同時改變同一個全域變數,可能產生競爭條件。 | 盡量使用 純函式,或在測試前後重置狀態(setup/teardown)。 |
最佳實踐
- 測試名稱具體:
fn test_add_overflow()比fn test1()更易於閱讀與除錯。 - 保持測試獨立:每個測試不依賴其他測試的執行結果。
- 使用
Result<(), String>:在需要檢查錯誤類型時,返回Result讓測試更具表現力。#[test] fn test_error_handling() -> Result<(), String> { let res = my_func(); if let Err(e) = res { assert_eq!(e.to_string(), "預期錯誤"); Ok(()) } else { Err("應該回傳錯誤".into()) } } - 把測試放在
src與tests兩層:單元測試放在src,整合測試放在tests/,兩者互補。 - 持續執行測試:在 CI/CD 流程中加入
cargo test --all --release,確保每次提交都通過測試。
實際應用場景
函式庫的 API 合約
- 使用
#[test]驗證公開函式在正常與邊界條件下的行為,確保升級不會破壞向後相容性。
- 使用
錯誤處理路徑
#[should_panic]可捕捉不合法參數、資源不足等情況,避免程式在正式環境中因未處理的 panic 而崩潰。
安全性檢查
- 在加密、序列化等需要嚴格檢查的模組中,使用
expected參數驗證 panic 訊息,確保錯誤來源正確。
- 在加密、序列化等需要嚴格檢查的模組中,使用
效能基準測試
- 雖然
#[test]本身不是效能測試工具,但可以結合criterion等 crate,先寫功能測試確保正確性,再加入基準測試。
- 雖然
跨平台相容
- 透過
#[cfg(target_os = "windows")]與#[cfg(test)]結合,寫出僅在特定平台執行的測試,確保平台差異不會影響核心功能。
- 透過
總結
#[test]是 Rust 測試框架的核心,讓我們能以 最小的設定 實現自動化測試。#[should_panic]為測試錯誤路徑提供了簡潔的寫法,配合expected參數可提升測試的精確度。- 正確的測試結構、具體的斷言與明確的命名,是維持測試可讀性與可維護性的關鍵。
- 避免常見陷阱、遵循最佳實踐,並將測試納入 CI/CD 流程,才能在開發過程中即時捕捉回歸與潛在缺陷。
透過本文的概念與範例,你已具備在 Rust 專案中自信地撰寫與維護測試的能力。持續練習、逐步擴充測試覆蓋率,將讓你的程式碼在品質與安全性上更上一層樓。祝你在 Rust 的測試旅程中玩得開心、寫得順利!