在軟體開發中,測試是確保程式碼質量與穩定性的關鍵步驟。今天要來介紹 Rust 提供的內建測試框架,讓我們能輕鬆撰寫單元測試和集成測試,確認程式是否如預期運作,我們會探討以下幾個主題:
panic!
測試Rust 的測試框架內建於標準庫,不需要額外安裝套件。只要寫一個函數並加上 #[test]
標記,Rust 便會自動將它識別為測試,然後在執行時確認它是否通過。
測試的概念就像你在開發時建立的一個小考試,你會輸入一些資料,然後看這些資料是否能通過預期的檢查。這樣可以在開發過程中防止錯誤的出現。
單元測試的主要目的是針對程式中的個別小功能進行測試,確保其輸入和輸出符合預期。這通常用於檢查單個函數或模組的邏輯,從而驗證小範圍的功能是否正常運作。單元測試非常重要,因為它能讓我們提早發現程式中的邏輯錯誤,避免在後續的開發過程中引發更大的問題。
假設我們有一個 add
函數,該函數的作用是將兩個整數相加。我們希望透過撰寫測試來確認這個函數的結果是否正確。
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
let result = add(2, 3);
assert_eq!(result, 5); // 檢查 add(2, 3) 的結果是否為 5
}
}
讓我們逐步分解這段程式碼,看看每一部分的具體功能。
pub fn add(a: i32, b: i32) -> i32
這是我們要測試的 add
函數。它接受兩個整數作為參數,並返回它們的和。這個函數本身非常簡單,但我們需要透過測試來確保它的邏輯正確,尤其是在多種輸入情況下。
#[cfg(test)]
這段程式碼是一個條件編譯指示。它告訴編譯器,只有在進行測試時才會編譯 tests
模組內的測試程式碼,這樣在生產環境中不會包含這些測試相關的程式碼。
mod tests
這是測試模組的定義。我們將測試函數放在這個模組中,並透過 use super::*;
引入我們要測試的 add
函數。這樣我們可以在測試中直接使用 add
函數,而不需要額外的模組路徑。
#[test]
#[test]
是 Rust 測試框架的核心屬性,它標記了這個函數為一個測試函數。當我們執行 cargo test
命令時,Rust 會自動找到所有帶有 #[test]
屬性的函數並執行它們。這些測試函數應該包含斷言來檢查程式的行為是否符合預期。
fn test_add()
這是實際進行測試的函數。它首先執行我們要測試的 add
函數,並將結果儲存在 result
變數中。接著,它使用 assert_eq!
斷言巨集來檢查 result
是否等於預期值 5。
assert_eq!
:這是一個斷言巨集,用於檢查兩個值是否相等。如果值不相等,這個測試會失敗,並顯示錯誤訊息。這種斷言有助於確保程式的行為符合預期,並在問題發生時快速找到錯誤原因。在這個例子中,我們輸入了數字 2
和 3
,期望結果是 5
。如果 add
函數正確實現了相加功能,測試會通過。如果結果不是 5
,測試會失敗並顯示對應的錯誤訊息,讓我們可以迅速定位問題。
Rust 的測試框架內建在 Cargo 中,這意味著你不需要安裝任何額外的工具來運行測試。每次你執行測試時,Cargo 會自動編譯專案、運行測試函數,並為你顯示測試結果。
要運行測試,只需打開終端機並執行以下命令:
cargo test
這個命令會做幾件事情:
#[cfg(test)]
標記的測試模組。#[test]
標記的測試函數。假設你已經撰寫了一個名為 test_add
的測試,當你執行 cargo test
命令後,你可能會看到類似如下的輸出結果:
running 1 test
test tests::test_add ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; finished in 0.00s
讓我們逐步解析這個輸出:
running 1 test
:這行表示 Rust 找到並運行了一個測試函數。在這個例子中,只有一個測試函數,名為 test_add
。
test tests::test_add ... ok
:這行表示 test_add
測試成功通過了。ok
表示測試結果符合預期,並且測試沒有失敗。
test result: ok. 1 passed; 0 failed; 0 ignored;
:這段總結了測試結果:
1 passed
:表示有一個測試成功通過。0 failed
:表示沒有測試失敗。如果某個測試失敗了,這裡會顯示失敗的數量。0 ignored
:表示沒有忽略的測試。有時候,你可能會標記某些測試為 #[ignore]
,這些測試在默認情況下不會執行,除非你指定要執行忽略的測試。finished in 0.00s
:這段表示測試執行所花費的時間。在這個例子中,測試執行非常迅速,幾乎立刻完成。
假設測試失敗了,結果可能會如下所示:
running 1 test
test tests::test_add ... FAILED
failures:
---- tests::test_add stdout ----
thread 'tests::test_add' panicked at 'assertion failed: `(left == right)`
left: `4`,
right: `5`', src/lib.rs:8:9
failures:
tests::test_add
test result: FAILED. 0 passed; 1 failed; 0 ignored; finished in 0.00s
這裡顯示測試失敗,因為 assert_eq!
檢查的結果並非我們期望的值。具體錯誤訊息會告訴你哪些值不相等(left: 4
與 right: 5
),並指向錯誤發生的位置(src/lib.rs:8:9
)。
這樣的錯誤報告可以幫助你迅速發現和修正程式中的邏輯問題。
單元測試是用來驗證單個函數或模組的功能是否正常,而集成測試則是用來確認多個模組之間的協同工作是否符合預期。這種測試能幫助我們確保程式的不同部分能夠正確互動,避免在整合時發生意外的錯誤。
集成測試通常用於測試一個系統的某些核心流程,確保它們在多個模組間的合作下能夠正常運行。
在 Rust 中,集成測試通常放在專案的 tests
目錄下。這些測試是以單獨的文件形式存在,並且每個文件中的測試會當作獨立的二進位執行檔來運行。這與單元測試的區別在於,單元測試通常是在模組內部進行的,而集成測試則是從專案的外部來檢查功能。
首先,我們需要在專案的根目錄下創建一個名為 tests
的資料夾。如果資料夾已經存在,可以直接在其中新增測試文件。假設我們要測試的是 add
函數,可以創建 tests/integration_test.rs
檔案。
在 integration_test.rs
中,我們可以撰寫如下的測試:
// tests/integration_test.rs
use your_crate::add;
#[test]
fn test_add_integration() {
let result = add(5, 10);
assert_eq!(result, 15);
}
use rust_test::add
:這行代碼將從當前專案中導入 add
函數。在集成測試中,模組和函數會被當作外部的庫來使用,因此我們必須使用 use
語句來明確導入它們。你需要將 rust-test
替換為 rust_test
的專案名稱的來呼叫。
#[test]
:這裡依然使用了 Rust 測試框架中的 #[test]
屬性來標記測試函數。這告訴 Cargo 這個函數是一個測試,在執行 cargo test
時會被運行。
assert_eq!
:這是一個斷言,用來檢查兩個值是否相等。如果 add(5, 10)
的結果是 15
,那麼測試會通過。如果不是,測試會失敗,並顯示錯誤訊息。
當集成測試寫好後,你依然可以使用 cargo test
來執行測試。Rust 會自動識別並運行 tests
資料夾下的所有測試文件。
cargo test
這個命令會同時執行單元測試和集成測試。Rust 會自動區分專案中的單元測試(位於模組內部)和集成測試(位於 tests
資料夾),並逐一執行所有標記為 #[test]
的函數。
當你執行集成測試時,輸出結果會和單元測試類似。假設我們執行的集成測試通過了,結果可能如下:
running 1 test
test integration_test::test_add_integration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; finished in 0.00s
這表示有一個集成測試被成功執行,並且測試結果符合預期。
集成測試的目的是確認多個模組之間的協同作用是否符合預期。舉個例子,如果你開發了一個網路服務應用,單元測試可能會檢查單個模組的功能,例如用戶認證功能是否正確,而集成測試則會檢查整個系統的工作流程,比如用戶是否能夠正確註冊、登錄,並執行後續操作。
透過集成測試,你能夠提早發現模組之間的互動問題,確保整個系統的穩定性。
當我們在開發應用程式時,處理錯誤是非常重要的一環。程式必須能夠適當地應對錯誤情境,例如無效的輸入或不符合預期的行為。為了確保程式在錯誤情況下能夠按照預期運行,我們可以撰寫專門的測試來模擬這些錯誤場景。在 Rust 中,特別是針對會導致程式崩潰(panic!
)的情況,我們可以使用 #[should_panic]
屬性來標記預期會發生錯誤的測試。
在某些情況下,程式的行為可能會產生錯誤,這可能是因為無效的輸入,或是程式中的特定邏輯分支引發了異常。為了確保這些錯誤情況被適當處理,我們需要撰寫測試來模擬這些情境,並驗證程式是否能按預期進行錯誤處理。
panic!
測試在 Rust 中,當遇到不可恢復的錯誤時,程式會使用 panic!
來中止運行。舉個簡單的例子,當一個函數嘗試除以 0 時,這種情況是不合法的,我們應該讓程式發出 panic!
來避免這樣的操作。為了測試這種情境,我們可以使用 #[should_panic]
巨集,來告訴測試框架,我們預期這個測試會引發 panic!
。
首先,我們一樣在 tests
資料夾下建立 divide_test.rs
,並輸入以下程式碼:
pub fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("除數不能為 0");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "除數不能為 0")]
fn test_divide_by_zero() {
divide(10, 0);
}
}
#[should_panic]
:這個屬性告訴 Rust 測試框架,這個測試應該觸發 panic!
。當 divide
函數執行 panic!
時,測試會被認為成功。如果 panic!
沒有發生,測試將失敗。
expected = "除數不能為 0"
:這是一個選項,允許你指定預期的 panic!
訊息。如果測試中引發的 panic!
訊息與這裡指定的不符,測試也會失敗。這樣可以更加精確地檢查程式是否引發了正確的錯誤訊息。
在程式設計中,遇到錯誤情境時觸發 panic!
其實是應對不可預期錯誤的最後手段。通常,應該儘量避免在一般邏輯中引發 panic!
,而是使用錯誤處理機制(如 Result
或 Option
)來管理預期的錯誤。然而,當遇到無法避免或不可恢復的錯誤時(例如,嘗試除以 0 或訪問不存在的索引),使用 panic!
是一種合適的選擇。
例如,在數學運算中,除以 0 是不合法的操作,必須處理這種情況以防止程式崩潰。這時,我們可以撰寫一個觸發 panic!
的測試來驗證我們的程式能夠正確處理這個錯誤。
panic!
測試的意義撰寫 panic!
測試有助於確認程式在遇到無效操作時是否會引發正確的錯誤,並給出相應的錯誤訊息。這在應對極端情況或無效輸入時尤為重要,可以防止潛在的崩潰或未預期的錯誤行為蔓延至整個應用程式。
假設 divide
函數沒有對除數為 0 的情況觸發 panic!
,這時測試會失敗,並顯示錯誤訊息,說明我們的測試未能達到預期結果。
thread 'tests::test_divide_by_zero' panicked at 'assertion failed: expected panic, but no panic occurred', src/lib.rs:8:9
這種情況下,我們就知道必須修正程式,使其在遇到無效除數時觸發正確的錯誤。
單元測試的目的是針對程式中的「最小單位」進行測試,這通常是指函數或類別的單一邏輯。這些測試應該簡單、快速,專注於確認某個小功能是否能按預期運作。
測試單一功能的正確性:如果你剛剛完成了一個函數,想確認它能夠在特定輸入下回傳正確結果,這是單元測試最常見的情況。它有助於快速驗證單一邏輯的正確性,而不需要考慮外部依賴。
例子:
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
測試邊界條件與特殊輸入:對於任何會接受用戶輸入的功能,測試極端情況或無效輸入是必須的。例如,當你開發數學運算或處理字串的函數時,單元測試能幫助你確認函數在極限情況下的表現。
例子:測試空字串、負數或除以零的情況。
#[test]
fn test_divide_by_zero() {
assert!(divide(10, 0).is_err());
}
快速驗證回歸錯誤:當你修復了一個錯誤時,單元測試能確保修復後該功能不再出現同樣的問題,並且在未來的開發過程中不會因其他改動引發回歸錯誤。
集成測試的重點在於檢查多個模組之間的協同運作。當系統越來越複雜時,單元測試無法涵蓋各模組之間的互動,因此需要透過集成測試來檢查整體流程是否符合預期。
測試系統的工作流程:當你想測試一個完整的工作流程(例如,從用戶註冊到數據儲存的整個過程),集成測試可以檢查模組之間的交互是否正常,確保它們能夠無縫合作。
例子:測試從前端 API 發送請求到後端數據庫操作是否正確連接。
#[test]
fn test_complete_workflow() {
let result = complete_user_registration("test_user");
assert_eq!(result, true);
}
檢查模組之間的依賴性:當你的應用程式依賴多個模組或第三方庫時,集成測試能確保這些依賴能正確協作。例如,確認數據從資料庫中提取、處理後,能正確顯示在前端。
測試外部服務的交互:如果你的程式需要與外部 API 或其他系統進行溝通,集成測試可以模擬這些交互情境,確保整個系統能正確處理外部依賴帶來的挑戰。
Rust 提供了內建測試框架,讓開發者能夠確保程式碼的穩定性與品質。在這篇文章中,我們學會了如何撰寫單元測試、集成測試,處理錯誤情境,以及如何使用測試框架中的各種功能。
在vscode編輯器當中,如果你設定了 #[test]
則 rust-analyzer
會判斷該函數為測試函數,因此可以直接點程式碼內的 Run Test
來進行測試,vscode會直接開啟一個terminal展示測試成果,這是一個非常方便的功能喔。
現在,你可以開始使用 Rust 測試框架,為你的專案撰寫測試,確保程式能夠穩定運行,並在開發過程中不斷驗證程式的正確性。