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