本文 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)。

最佳實踐

  1. 測試名稱具體fn test_add_overflow()fn test1() 更易於閱讀與除錯。
  2. 保持測試獨立:每個測試不依賴其他測試的執行結果。
  3. 使用 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())
        }
    }
    
  4. 把測試放在 srctests 兩層:單元測試放在 src,整合測試放在 tests/,兩者互補。
  5. 持續執行測試:在 CI/CD 流程中加入 cargo test --all --release,確保每次提交都通過測試。

實際應用場景

  1. 函式庫的 API 合約

    • 使用 #[test] 驗證公開函式在正常與邊界條件下的行為,確保升級不會破壞向後相容性。
  2. 錯誤處理路徑

    • #[should_panic] 可捕捉不合法參數、資源不足等情況,避免程式在正式環境中因未處理的 panic 而崩潰。
  3. 安全性檢查

    • 在加密、序列化等需要嚴格檢查的模組中,使用 expected 參數驗證 panic 訊息,確保錯誤來源正確。
  4. 效能基準測試

    • 雖然 #[test] 本身不是效能測試工具,但可以結合 criterion 等 crate,先寫功能測試確保正確性,再加入基準測試。
  5. 跨平台相容

    • 透過 #[cfg(target_os = "windows")]#[cfg(test)] 結合,寫出僅在特定平台執行的測試,確保平台差異不會影響核心功能。

總結

  • #[test] 是 Rust 測試框架的核心,讓我們能以 最小的設定 實現自動化測試。
  • #[should_panic] 為測試錯誤路徑提供了簡潔的寫法,配合 expected 參數可提升測試的精確度。
  • 正確的測試結構、具體的斷言與明確的命名,是維持測試可讀性與可維護性的關鍵。
  • 避免常見陷阱、遵循最佳實踐,並將測試納入 CI/CD 流程,才能在開發過程中即時捕捉回歸與潛在缺陷。

透過本文的概念與範例,你已具備在 Rust 專案中自信地撰寫與維護測試的能力。持續練習、逐步擴充測試覆蓋率,將讓你的程式碼在品質與安全性上更上一層樓。祝你在 Rust 的測試旅程中玩得開心、寫得順利!