ch11-01-writing-tests.md
commit b9a473ff80e72ed9a77f97a80799b5aff25b594a
Rust 中的測試函數(shù)是用來驗證非測試代碼是否按照期望的方式運行的。測試函數(shù)體通常執(zhí)行如下三種操作:
讓我們看看 Rust 提供的專門用來編寫測試的功能:?test
? 屬性、一些宏和 ?should_panic
? 屬性。
作為最簡單例子,Rust 中的測試就是一個帶有 test
屬性注解的函數(shù)。屬性(attribute)是關于 Rust 代碼片段的元數(shù)據(jù);第五章中結構體中用到的 derive
屬性就是一個例子。為了將一個函數(shù)變成測試函數(shù),需要在 fn
行之前加上 #[test]
。當使用 cargo test
命令運行測試時,Rust 會構建一個測試執(zhí)行程序用來調用標記了 test
屬性的函數(shù),并報告每一個測試是通過還是失敗。
第七章當使用 Cargo 新建一個庫項目時,它會自動為我們生成一個測試模塊和一個測試函數(shù)。這有助于我們開始編寫測試,因為這樣每次開始新項目時不必去查找測試函數(shù)的具體結構和語法了。當然你也可以額外增加任意多的測試函數(shù)以及測試模塊!
我們會通過實驗那些自動生成的測試模版而不是實際編寫測試代碼來探索測試如何工作的一些方面。接著,我們會寫一些真正的測試,調用我們編寫的代碼并斷言他們的行為的正確性。
讓我們創(chuàng)建一個新的庫項目 adder
:
$ cargo new adder --lib
Created library `adder` project
$ cd adder
adder 庫中 src/lib.rs
的內容應該看起來如示例 11-1 所示:
文件名: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
示例 11-1:由 cargo new
自動生成的測試模塊和函數(shù)
現(xiàn)在讓我們暫時忽略 tests
模塊和 #[cfg(test)]
注解,并只關注函數(shù)來了解其如何工作。注意 fn
行之前的 #[test]
:這個屬性表明這是一個測試函數(shù),這樣測試執(zhí)行者就知道將其作為測試處理。因為也可以在 tests
模塊中擁有非測試的函數(shù)來幫助我們建立通用場景或進行常見操作,所以需要使用 #[test]
屬性標明哪些函數(shù)是測試。
函數(shù)體通過使用 assert_eq!
宏來斷言 2 加 2 等于 4。一個典型的測試的格式,就是像這個例子中的斷言一樣。接下來運行就可以看到測試通過。
cargo test
命令會運行項目中所有的測試,如示例 11-2 所示:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.57s
Running unittests (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
示例 11-2:運行自動生成測試的輸出
Cargo 編譯并運行了測試。在 Compiling
、Finished
和 Running
這幾行之后,可以看到 running 1 test
這一行。下一行顯示了生成的測試函數(shù)的名稱,它是 it_works
,以及測試的運行結果,ok
。接著可以看到全體測試運行結果的摘要:test result: ok.
意味著所有測試都通過了。1 passed; 0 failed
表示通過或失敗的測試數(shù)量。
因為之前我們并沒有將任何測試標記為忽略,所以摘要中會顯示 0 ignored
。我們也沒有過濾需要運行的測試,所以摘要中會顯示0 filtered out
。在下一部分 “控制測試如何運行” 會討論忽略和過濾測試。
0 measured
統(tǒng)計是針對性能測試的。性能測試(benchmark tests)在編寫本書時,仍只能用于 Rust 開發(fā)版(nightly Rust)。請查看 性能測試的文檔 了解更多。
測試輸出中的以 Doc-tests adder
開頭的這一部分是所有文檔測試的結果。我們現(xiàn)在并沒有任何文檔測試,不過 Rust 會編譯任何在 API 文檔中的代碼示例。這個功能幫助我們使文檔和代碼保持同步!在第十四章的 “文檔注釋作為測試” 部分會講到如何編寫文檔測試。現(xiàn)在我們將忽略 Doc-tests
部分的輸出。
讓我們改變測試的名稱并看看這如何改變測試的輸出。給 it_works
函數(shù)起個不同的名字,比如 exploration
,像這樣:
文件名: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn exploration() {
assert_eq!(2 + 2, 4);
}
}
并再次運行 cargo test
?,F(xiàn)在輸出中將出現(xiàn) exploration
而不是 it_works
:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.59s
Running unittests (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
讓我們增加另一個測試,不過這一次是一個會失敗的測試!當測試函數(shù)中出現(xiàn) panic 時測試就失敗了。每一個測試都在一個新線程中運行,當主線程發(fā)現(xiàn)測試線程異常了,就將對應測試標記為失敗。第九章講到了最簡單的造成 panic 的方法:調用 panic!
宏。寫入新測試 another
后, src/lib.rs
現(xiàn)在看起來如示例 11-3 所示:
文件名: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn exploration() {
assert_eq!(2 + 2, 4);
}
#[test]
fn another() {
panic!("Make this test fail");
}
}
示例 11-3:增加第二個因調用了 panic!
而失敗的測試
再次 cargo test
運行測試。輸出應該看起來像示例 11-4,它表明 exploration
測試通過了而 another
失敗了:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.72s
Running unittests (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok
failures:
---- tests::another stdout ----
thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::another
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
示例 11-4:一個測試通過和一個測試失敗的測試結果
test tests::another
這一行是 FAILED
而不是 ok
了。在單獨測試結果和摘要之間多了兩個新的部分:第一個部分顯示了測試失敗的詳細原因。在這個例子中,another
因為在src/lib.rs 的第 10 行 panicked at 'Make this test fail'
而失敗。下一部分列出了所有失敗的測試,這在有很多測試和很多失敗測試的詳細輸出時很有幫助。我們可以通過使用失敗測試的名稱來只運行這個測試,以便調試;下一部分 “控制測試如何運行” 會講到更多運行測試的方法。
最后是摘要行:總體上講,測試結果是 FAILED
。有一個測試通過和一個測試失敗。
現(xiàn)在我們見過不同場景中測試結果是什么樣子的了,再來看看除 panic!
之外的一些在測試中有幫助的宏吧。
assert!
宏由標準庫提供,在希望確保測試中一些條件為 true
時非常有用。需要向 assert!
宏提供一個求值為布爾值的參數(shù)。如果值是 true
,assert!
什么也不做,同時測試會通過。如果值為 false
,assert!
調用 panic!
宏,這會導致測試失敗。assert!
宏幫助我們檢查代碼是否以期望的方式運行。
回憶一下第五章中,示例 5-15 中有一個 Rectangle
結構體和一個 can_hold
方法,在示例 11-5 中再次使用他們。將他們放進 src/lib.rs 并使用 assert!
宏編寫一些測試。
文件名: src/lib.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
示例 11-5:第五章中 Rectangle
結構體和其 can_hold
方法
can_hold
方法返回一個布爾值,這意味著它完美符合 assert!
宏的使用場景。在示例 11-6 中,讓我們編寫一個 can_hold
方法的測試來作為練習,這里創(chuàng)建一個長為 8 寬為 7 的 Rectangle
實例,并假設它可以放得下另一個長為 5 寬為 1 的 Rectangle
實例:
文件名: src/lib.rs
#[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));
}
}#[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));
}
}
示例 11-6:一個 can_hold
的測試,檢查一個較大的矩形確實能放得下一個較小的矩形
注意在 tests
模塊中新增加了一行:use super::*;
。tests
是一個普通的模塊,它遵循第七章 “路徑用于引用模塊樹中的項” 部分介紹的可見性規(guī)則。因為這是一個內部模塊,要測試外部模塊中的代碼,需要將其引入到內部模塊的作用域中。這里選擇使用 glob 全局導入,以便在 tests
模塊中使用所有在外部模塊定義的內容。
我們將測試命名為 larger_can_hold_smaller
,并創(chuàng)建所需的兩個 Rectangle
實例。接著調用 assert!
宏并傳遞 larger.can_hold(&smaller)
調用的結果作為參數(shù)。這個表達式預期會返回 true
,所以測試應該通過。讓我們拭目以待!
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests (target/debug/deps/rectangle-6584c4561e48942e)
running 1 test
test tests::larger_can_hold_smaller ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
它確實通過了!再來增加另一個測試,這一回斷言一個更小的矩形不能放下一個更大的矩形:
文件名: src/lib.rs
#[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));
}
}
因為這里 can_hold
函數(shù)的正確結果是 false
,我們需要將這個結果取反后傳遞給 assert!
宏。因此 can_hold
返回 false
時測試就會通過:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
兩個通過的測試!現(xiàn)在讓我們看看如果引入一個 bug 的話測試結果會發(fā)生什么。將 can_hold
方法中比較長度時本應使用大于號的地方改成小于號:
// --snip--
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width < other.width && self.height > other.height
}
}
現(xiàn)在運行測試會產生:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok
failures:
---- tests::larger_can_hold_smaller stdout ----
thread 'main' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:28:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::larger_can_hold_smaller
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
我們的測試捕獲了 bug!因為 larger.length
是 8 而 smaller.length
是 5,can_hold
中的長度比較現(xiàn)在因為 8 不小于 5 而返回 false
。
測試功能的一個常用方法是將需要測試代碼的值與期望值做比較,并檢查是否相等??梢酝ㄟ^向 assert!
宏傳遞一個使用 ==
運算符的表達式來做到。不過這個操作實在是太常見了,以至于標準庫提供了一對宏來更方便的處理這些操作 —— assert_eq!
和 assert_ne!
。這兩個宏分別比較兩個值是相等還是不相等。當斷言失敗時他們也會打印出這兩個值具體是什么,以便于觀察測試 為什么 失敗,而 assert!
只會打印出它從 ==
表達式中得到了 false
值,而不是導致 false
的兩個值。
示例 11-7 中,讓我們編寫一個對其參數(shù)加二并返回結果的函數(shù) add_two
。接著使用 assert_eq!
宏測試這個函數(shù)。
文件名: src/lib.rs
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));
}
}
示例 11-7:使用 assert_eq!
宏測試 add_two
函數(shù)
測試通過了!
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running unittests (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
傳遞給 assert_eq!
宏的第一個參數(shù) 4
,等于調用 add_two(2)
的結果。測試中的這一行 test tests::it_adds_two ... ok
中 ok
表明測試通過!
在代碼中引入一個 bug 來看看使用 assert_eq!
的測試失敗是什么樣的。修改 add_two
函數(shù)的實現(xiàn)使其加 3:
pub fn add_two(a: i32) -> i32 {
a + 3
}
再次運行測試:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.61s
Running unittests (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... FAILED
failures:
---- tests::it_adds_two stdout ----
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `4`,
right: `5`', src/lib.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_adds_two
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
測試捕獲到了 bug!it_adds_two
測試失敗,顯示信息 assertion failed: `(left == right)`
并表明 left
是 4
而 right
是 5
。這個信息有助于我們開始調試:它說 assert_eq!
的 left
參數(shù)是 4
,而 right
參數(shù),也就是 add_two(2)
的結果,是 5
。
需要注意的是,在一些語言和測試框架中,斷言兩個值相等的函數(shù)的參數(shù)叫做 expected
和 actual
,而且指定參數(shù)的順序是很關鍵的。然而在 Rust 中,他們則叫做 left
和 right
,同時指定期望的值和被測試代碼產生的值的順序并不重要。這個測試中的斷言也可以寫成 assert_eq!(add_two(2), 4)
,這時失敗信息會變成 assertion failed: `(left == right)`
其中 left
是 5
而 right
是 4
。
assert_ne!
宏在傳遞給它的兩個值不相等時通過,而在相等時失敗。在代碼按預期運行,我們不確定值 會 是什么,不過能確定值絕對 不會 是什么的時候,這個宏最有用處。例如,如果一個函數(shù)保證會以某種方式改變其輸出,不過這種改變方式是由運行測試時是星期幾來決定的,這時最好的斷言可能就是函數(shù)的輸出不等于其輸入。
assert_eq!
和 assert_ne!
宏在底層分別使用了 ==
和 !=
。當斷言失敗時,這些宏會使用調試格式打印出其參數(shù),這意味著被比較的值必需實現(xiàn)了 PartialEq
和 Debug
trait。所有的基本類型和大部分標準庫類型都實現(xiàn)了這些 trait。對于自定義的結構體和枚舉,需要實現(xiàn) PartialEq
才能斷言他們的值是否相等。需要實現(xiàn) Debug
才能在斷言失敗時打印他們的值。因為這兩個 trait 都是派生 trait,如第五章示例 5-12 所提到的,通??梢灾苯釉诮Y構體或枚舉上添加 #[derive(PartialEq, Debug)]
注解。附錄 C “可派生 trait” 中有更多關于這些和其他派生 trait 的詳細信息。
你也可以向 assert!、assert_eq! 和 assert_ne! 宏傳遞一個可選的失敗信息參數(shù),可以在測試失敗時將自定義失敗信息一同打印出來。任何在 assert! 的一個必需參數(shù)和 assert_eq! 和 assert_ne! 的兩個必需參數(shù)之后指定的參數(shù)都會傳遞給 format! 宏(在第八章的 “使用 + 運算符或 format! 宏拼接字符串” 部分討論過),所以可以傳遞一個包含 {} 占位符的格式字符串和需要放入占位符的值。自定義信息有助于記錄斷言的意義;當測試失敗時就能更好的理解代碼出了什么問題。
例如,比如說有一個根據(jù)人名進行問候的函數(shù),而我們希望測試將傳遞給函數(shù)的人名顯示在輸出中:
文件名: src/lib.rs
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
函數(shù)返回的確切值,我們將僅僅斷言輸出的文本中包含輸入?yún)?shù)。
讓我們通過將 greeting
改為不包含 name
來在代碼中引入一個 bug 來測試失敗時是怎樣的:
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
運行測試會產生:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished test [unoptimized + debuginfo] target(s) in 0.91s
Running unittests (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'assertion failed: result.contains(\"Carol\")', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
結果僅僅告訴了我們斷言失敗了和失敗的行號。一個更有用的失敗信息應該打印出 greeting
函數(shù)的值。讓我們?yōu)闇y試函數(shù)增加一個自定義失敗信息參數(shù):帶占位符的格式字符串,以及 greeting
函數(shù)的值:
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`",
result
);
}
現(xiàn)在如果再次運行測試,將會看到更有價值的信息:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished test [unoptimized + debuginfo] target(s) in 0.93s
Running unittests (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
可以在測試輸出中看到所取得的確切的值,這會幫助我們理解真正發(fā)生了什么,而不是期望發(fā)生什么。
除了檢查代碼是否返回期望的正確的值之外,檢查代碼是否按照期望處理錯誤也是很重要的。例如,考慮第九章示例 9-10 創(chuàng)建的 Guess
類型。其他使用 Guess
的代碼都是基于 Guess
實例僅有的值范圍在 1 到 100 的前提。可以編寫一個測試來確保創(chuàng)建一個超出范圍的值的 Guess
實例會 panic。
可以通過對函數(shù)增加另一個屬性 should_panic
來實現(xiàn)這些。這個屬性在函數(shù)中的代碼 panic 時會通過,而在其中的代碼沒有 panic 時失敗。
示例 11-8 展示了一個檢查 Guess::new
是否按照我們的期望出錯的測試:
文件名: src/lib.rs
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);
}
}
示例 11-8:測試會造成 panic!
的條件
#[should_panic]
屬性位于 #[test]
之后,對應的測試函數(shù)之前。讓我們看看測試通過時它是什么樣子:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running unittests (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests guessing_game
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
看起來不錯!現(xiàn)在在代碼中引入 bug,移除 new
函數(shù)在值大于 100 時會 panic 的條件:
// --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 }
}
}
如果運行示例 11-8 的測試,它會失?。?br>
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.62s
Running unittests (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
note: test did not panic as expected
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
這回并沒有得到非常有用的信息,不過一旦我們觀察測試函數(shù),會發(fā)現(xiàn)它標注了 #[should_panic]
。這個錯誤意味著代碼中測試函數(shù) Guess::new(200)
并沒有產生 panic。
然而 should_panic
測試結果可能會非常含糊不清,因為它只是告訴我們代碼并沒有產生 panic。should_panic
甚至在一些不是我們期望的原因而導致 panic 時也會通過。為了使 should_panic
測試結果更精確,我們可以給 should_panic
屬性增加一個可選的 expected
參數(shù)。測試工具會確保錯誤信息中包含其提供的文本。例如,考慮示例 11-9 中修改過的 Guess
,這里 new
函數(shù)根據(jù)其值是過大還或者過小而提供不同的 panic 信息:
文件名: src/lib.rs
// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be greater than or equal to 1, got {}.",
value
);
} else if value > 100 {
panic!(
"Guess value must be less than or equal to 100, got {}.",
value
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Guess value must be less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
示例 11-9:一個會帶有特定錯誤信息的 panic!
條件的測試
這個測試會通過,因為 should_panic
屬性中 expected
參數(shù)提供的值是 Guess::new
函數(shù) panic 信息的子串。我們可以指定期望的整個 panic 信息,在這個例子中是 Guess value must be less than or equal to 100, got 200.
。 expected
信息的選擇取決于 panic 信息有多獨特或動態(tài),和你希望測試有多準確。在這個例子中,錯誤信息的子字符串足以確保函數(shù)在 else if value > 100
的情況下運行。
為了觀察帶有 expected
信息的 should_panic
測試失敗時會發(fā)生什么,讓我們再次引入一個 bug,將 if value < 1
和 else if value > 100
的代碼塊對換:
if value < 1 {
panic!(
"Guess value must be less than or equal to 100, got {}.",
value
);
} else if value > 100 {
panic!(
"Guess value must be greater than or equal to 1, got {}.",
value
);
}
這一次運行 should_panic
測試,它會失?。?br>
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
thread 'main' panicked at 'Guess value must be greater than or equal to 1, got 200.', src/lib.rs:13:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: `"Guess value must be greater than or equal to 1, got 200."`,
expected substring: `"Guess value must be less than or equal to 100"`
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
失敗信息表明測試確實如期望 panic 了,不過 panic 信息中并沒有包含 expected
信息 'Guess value must be less than or equal to 100'
。而我們得到的 panic 信息是 'Guess value must be greater than or equal to 1, got 200.'
。這樣就可以開始尋找 bug 在哪了!
目前為止,我們編寫的測試在失敗時就會 panic。也可以使用 Result<T, E>
編寫測試!這里是第一個例子采用了 Result:
#[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"))
}
}
}
現(xiàn)在 it_works
函數(shù)的返回值類型為 Result<(), String>
。在函數(shù)體中,不同于調用 assert_eq!
宏,而是在測試通過時返回 Ok(())
,在測試失敗時返回帶有 String
的 Err
。
這樣編寫測試來返回 Result<T, E>
就可以在函數(shù)體中使用問號運算符,如此可以方便的編寫任何運算符會返回 Err
成員的測試。
不能對這些使用 Result<T, E>
的測試使用 #[should_panic]
注解。為了斷言一個操作返回 Err
成員,不要使用對 Result<T, E>
值使用問號表達式(?
)。而是使用 assert!(value.is_err())
。
現(xiàn)在你知道了幾種編寫測試的方法,讓我們看看運行測試時會發(fā)生什么,和可以用于 cargo test
的不同選項。
更多建議: