Rust 是一個相當注重正確性的編程語言,不過正確性是一個難以證明的復雜主題。Rust 的類型系統在此問題上下了很大的功夫,不過它不可能捕獲所有種類的錯誤。為此,Rust 也在語言本身包含了編寫軟件測試的支持。
編寫一個叫做?add_two
?的將傳遞給它的值加二的函數。它的簽名有一個整型參數并返回一個整型值。當實現和編譯這個函數時,Rust 會進行所有目前我們已經見過的類型檢查和借用檢查,例如,這些檢查會確保我們不會傳遞?String
?或無效的引用給這個函數。Rust 所?不能?檢查的是這個函數是否會準確的完成我們期望的工作:返回參數加二后的值,而不是比如說參數加 10 或減 50 的值!這也就是測試出場的地方。
可以編寫測試斷言,比如說,當傳遞?3
?給?add_two
?函數時,返回值是?5
。無論何時對代碼進行修改,都可以運行測試來確保任何現存的正確行為沒有被改變。
11.1?編寫測試
如何編寫測試
Rust 中的測試函數是用來驗證非測試代碼是否按照期望的方式運行的。測試函數體通常執行如下三種操作:
- 設置任何所需的數據或狀態
- 運行需要測試的代碼
- 斷言其結果是我們所期望的
測試函數剖析
作為最簡單例子,Rust 中的測試就是一個帶有?test
?屬性注解的函數。屬性(attribute)是關于 Rust 代碼片段的元數據;第五章中結構體中用到的?derive
?屬性就是一個例子。為了將一個函數變成測試函數,需要在?fn
?行之前加上?#[test]
。當使用?cargo test
?命令運行測試時,Rust 會構建一個測試執行程序用來調用標記了?test
?屬性的函數,并報告每一個測試是通過還是失敗。
創建一個新的庫項目?adder
:
$ cargo new adder --libCreated library `adder` project
$ cd adder
新建后的默認代碼是,判斷加法
pub fn add(left: usize, right: usize) -> usize {left + right
}#[cfg(test)]
mod tests {use super::*;#[test]fn it_works() {let result = add(2, 2);assert_eq!(result, 4);}
}
使用
cargo test
結果
Cargo 編譯并運行了測試。在?Compiling
、Finished
?和?Running
?這幾行之后,可以看到?running 1 test
?這一行。下一行顯示了生成的測試函數的名稱,它是?it_works
,以及測試的運行結果,ok
。接著可以看到全體測試運行結果的摘要:test result: ok.
?意味著所有測試都通過了。1 passed; 0 failed
?表示通過或失敗的測試數量。
因為之前我們并沒有將任何測試標記為忽略,所以摘要中會顯示?0 ignored
。我們也沒有過濾需要運行的測試,所以摘要中會顯示0 filtered out
。
0 measured
?統計是針對性能測試的。性能測試(benchmark tests)在編寫本書時,仍只能用于 Rust 開發版(nightly Rust)。
測試輸出中的以?Doc-tests adder
?開頭的這一部分是所有文檔測試的結果。我們現在并沒有任何文檔測試,不過 Rust 會編譯任何在 API 文檔中的代碼示例。這個功能幫助我們使文檔和代碼保持同步!
改變測試的名稱并看看這如何改變測試的輸出。修改測名稱
pub fn add(left: usize, right: usize) -> usize {left + right
}#[cfg(test)]
mod tests {use super::*;#[test]// 這里修改了測試名稱fn exploration() {let result = add(2, 2);assert_eq!(result, 4);}
}
結果
讓我們增加另一個測試,不過這一次是一個會失敗的測試!當測試函數中出現 panic 時測試就失敗了。每一個測試都在一個新線程中運行,當主線程發現測試線程異常了,就將對應測試標記為失敗。第九章講到了最簡單的造成 panic 的方法:調用?panic!
?宏。
pub fn add(left: usize, right: usize) -> usize {left + right
}#[cfg(test)]
mod tests {use super::*;#[test]fn exploration() {let result = add(2, 2);assert_eq!(result, 4);}// 新增錯誤測試#[test]fn another() {panic!("Make this test fail");}}
結果
再次?cargo test
?運行測試。它表明?exploration
?測試通過了而?another
?失敗了
test tests::another
?這一行是?FAILED
?而不是?ok
?了。在單獨測試結果和摘要之間多了兩個新的部分:第一個部分顯示了測試失敗的詳細原因。在這個例子中,another
?因為在src/lib.rs?的第 10 行?panicked at 'Make this test fail'
?而失敗。下一部分列出了所有失敗的測試,這在有很多測試和很多失敗測試的詳細輸出時很有幫助。
最后是摘要行:總體上講,測試結果是?FAILED
。有一個測試通過和一個測試失敗。
使用assert!宏來檢查結果
assert!
?宏由標準庫提供,在希望確保測試中一些條件為?true
?時非常有用。需要向?assert!
?宏提供一個求值為布爾值的參數。如果值是?true
,assert!
?什么也不做,同時測試會通過。如果值為?false
,assert!
?調用?panic!
?宏,這會導致測試失敗。assert!
?宏幫助我們檢查代碼是否以期望的方式運行。
// 結構體
struct Rectangle {width: u32,height: u32,
}// 結構體實現了can_hold方法
impl Rectangle {fn can_hold(&self, other: &Rectangle) -> bool {self.width > other.width && self.height > other.height}
}// 測試
#[cfg(test)]
mod tests {use super::*;#[test]fn larger_can_hold_smaller() {let larger = Rectangle { width: 8, height: 7 };let smaller = Rectangle { width: 5, height: 1 };assert!(larger.can_hold(&smaller));}
}
注意在?tests
?模塊中新增加了一行:use super::*;
。
我們將測試命名為?larger_can_hold_smaller
,并創建所需的兩個?Rectangle
?實例。接著調用?assert!
?宏并傳遞?larger.can_hold(&smaller)
?調用的結果作為參數。這個表達式預期會返回?true
,所以測試應該通過。
結果
再來增加另一個測試,這一回斷言一個更小的矩形不能放下一個更大的矩形:
fn main() {}
#[cfg(test)]
mod tests {use super::*;#[test]fn larger_can_hold_smaller() {// --snip--}#[test]fn smaller_cannot_hold_larger() {let larger = Rectangle { width: 8, height: 7 };let smaller = Rectangle { width: 5, height: 1 };assert!(!smaller.can_hold(&larger));}
}
?也通過了
如果引入一個 bug 的話測試結果會發生什么。將?can_hold
?方法中比較長度時本應使用大于號的地方改成小于號:
impl Rectangle {fn can_hold(&self, other: &Rectangle) -> bool {self.width < other.width && self.height > other.height}
}
結果
我們的測試捕獲了 bug!因為?larger.length
?是 8 而?smaller.length
?是 5,can_hold
?中的長度比較現在因為 8 不小于 5 而返回?false
。
使用assert_eq!和assert_ne!宏來測試相等
測試功能的一個常用方法是將需要測試代碼的值與期望值做比較,并檢查是否相等。可以通過向?assert!
?宏傳遞一個使用?==
?運算符的表達式來做到。不過這個操作實在是太常見了,以至于標準庫提供了一對宏來更方便的處理這些操作 ——?assert_eq!
?和?assert_ne!
。這兩個宏分別比較兩個值是相等還是不相等。當斷言失敗時他們也會打印出這兩個值具體是什么,以便于觀察測試?為什么?失敗,而?assert!
?只會打印出它從?==
?表達式中得到了?false
?值,而不是導致?false
?的兩個值。
pub fn add_two(a: i32) -> i32 {a + 2
}#[cfg(test)]
mod tests {use super::*;#[test]fn it_adds_two() {assert_eq!(4, add_two(2));}
}
傳遞給?assert_eq!
?宏的第一個參數?4
?,等于調用?add_two(2)
?的結果。測試中的這一行?test tests::it_adds_two ... ok
?中?ok
?表明測試通過!
在代碼中引入一個 bug 來看看使用?assert_eq!
?的測試失敗是什么樣的。
pub fn add_two(a: i32) -> i32 {a + 3 // 這里修改了
}#[cfg(test)]
mod tests {use super::*;#[test]fn it_adds_two() {assert_eq!(4, add_two(2));}
}
結果
測試捕獲到了 bug!it_adds_two
?測試失敗,顯示信息?assertion failed: `(left == right)`
?并表明?left
?是?4
?而?right
?是?5
。這個信息有助于我們開始調試:它說?assert_eq!
?的?left
?參數是?4
,而?right
?參數,也就是?add_two(2)
?的結果,是?5
。
需要注意的是,在一些語言和測試框架中,斷言兩個值相等的函數的參數叫做?expected
?和?actual
,而且指定參數的順序是很關鍵的。然而在 Rust 中,他們則叫做?left
?和?right
,同時指定期望的值和被測試代碼產生的值的順序并不重要。這個測試中的斷言也可以寫成?assert_eq!(add_two(2), 4)
,這時失敗信息會變成?assertion failed: `(left == right)`
?其中?left
?是?5
?而?right
?是?4
。
assert_ne!
?宏在傳遞給它的兩個值不相等時通過,而在相等時失敗。
自定義失敗信息
也可以向?assert!
、assert_eq!
?和?assert_ne!
?宏傳遞一個可選的失敗信息參數,可以在測試失敗時將自定義失敗信息一同打印出來。任何在?assert!
?的一個必需參數和?assert_eq!
?和?assert_ne!
?的兩個必需參數之后指定的參數都會傳遞給?format!
?宏,所以可以傳遞一個包含?{}
?占位符的格式字符串和需要放入占位符的值。自定義信息有助于記錄斷言的意義;當測試失敗時就能更好的理解代碼出了什么問題。
例如,比如說有一個根據人名進行問候的函數,而我們希望測試將傳遞給函數的人名顯示在輸出中:
?
pub fn greeting(name: &str) -> String {format!("Hello {}!", name)
}#[cfg(test)]
mod tests {use super::*;#[test]fn greeting_contains_name() {let result = greeting("Carol");assert!(result.contains("Carol"));}
}
結果
這個程序的需求還沒有被確定,因此問候文本開頭的?Hello
?文本很可能會改變。然而我們并不想在需求改變時不得不更新測試,所以相比檢查?greeting
?函數返回的確切值,我們將僅僅斷言輸出的文本中包含輸入參數。
讓我們通過將?greeting
?改為不包含?name
?來在代碼中引入一個 bug 來測試失敗時是怎樣的:
pub fn greeting(name: &str) -> String {String::from("Hello!")
}
結果
結果僅僅告訴了我們斷言失敗了和失敗的行號。一個更有用的失敗信息應該打印出?greeting
?函數的值。讓我們為測試函數增加一個自定義失敗信息參數:帶占位符的格式字符串,以及?greeting
?函數的值:
#[test]
fn greeting_contains_name() {let result = greeting("Carol");assert!(result.contains("Carol"),"Greeting did not contain name, value was `{}`", result);
}
結果
使用should_panic檢查panic
除了檢查代碼是否返回期望的正確的值之外,檢查代碼是否按照期望處理錯誤也是很重要的。
可以通過對函數增加另一個屬性?should_panic
?來實現這些。這個屬性在函數中的代碼 panic 時會通過,而在其中的代碼沒有 panic 時失敗。
pub struct Guess {value: i32,
}impl Guess {pub fn new(value: i32) -> Guess {if value < 1 || value > 100 {panic!("Guess value must be between 1 and 100, got {}.", value);}Guess {value}}
}#[cfg(test)]
mod tests {use super::*;#[test]#[should_panic]fn greater_than_100() {Guess::new(200);}
}
結果
看起來不錯!現在在代碼中引入 bug,移除?
new
?函數在值大于 100 時會 panic 的條件:
fn main() {}
pub struct Guess {value: i32,
}// --snip--impl Guess {pub fn new(value: i32) -> Guess {if value < 1 {panic!("Guess value must be between 1 and 100, got {}.", value);}Guess {value}}
}
結果
這回并沒有得到非常有用的信息,不過一旦我們觀察測試函數,會發現它標注了?#[should_panic]
。這個錯誤意味著代碼中測試函數?Guess::new(200)
?并沒有產生 panic。
將Result<T,E>用于測試
也可以使用?Result<T, E>
?編寫測試!這里是第一個例子采用了 Result:
#![allow(unused_variables)]
fn main() {
#[cfg(test)]
mod tests {#[test]fn it_works() -> Result<(), String> {if 2 + 2 == 4 {Ok(())} else {Err(String::from("two plus two does not equal four"))}}
}
}
現在?it_works
?函數的返回值類型為?Result<(), String>
。在函數體中,不同于調用?assert_eq!
?宏,而是在測試通過時返回?Ok(())
,在測試失敗時返回帶有?String
?的?Err
。
這樣編寫測試來返回?Result<T, E>
?就可以在函數體中使用問號運算符,如此可以方便的編寫任何運算符會返回?Err
?成員的測試。
不能對這些使用?Result<T, E>
?的測試使用?#[should_panic]
?注解。相反應該在測試失敗時直接返回?Err
?值。
11.2?運行測試
11.3?測試的組織結構
用到再學
參考:?測試 - Rust 程序設計語言 簡體中文版 (bootcss.com)