程序測試是一個(gè)非常有效的方法,它可以有效的暴漏程序中的缺陷,但對于暴漏缺陷來說,這還是遠(yuǎn)遠(yuǎn)不夠的。
—— Edsger W. Dijkstra,"卑微的程序員" (1972)
讓我們來談?wù)勅绾螠y試 Rust 代碼。我們將談?wù)摬皇鞘裁礈y試 Rust 代碼正確的方法。關(guān)于正確和錯(cuò)誤地編寫測試的方式有很多的流派。所有這些方法都使用相同的基本工具,因此,我們將向您展示使用它們的語法。
Rust 中一個(gè)最簡單的測試是一個(gè)函數(shù),它使用 test 屬性注釋。讓我們使用 Cargo 做一個(gè)叫加法器的新項(xiàng)目:
$ cargo new adder
$ cd adder
當(dāng)你做一個(gè)新項(xiàng)目時(shí),Cargo 將自動(dòng)生成一個(gè)簡單的測試。下面即是 src/lib.rs 的內(nèi)容:
#[test]
fn it_works() {
}
注意 #[test]
。該屬性表明,這是一個(gè)測試函數(shù),目前還沒有函數(shù)體。我們可以使用 Cargo test 運(yùn)行這個(gè)測試:
$ cargo test
Compiling adder v0.0.1 (file:///home/you/projects/adder)
Running target/adder-91b3e234d4ed382a
running 1 test
test it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Cargo 編譯和運(yùn)行我們的測試。這里有兩組輸出:一個(gè)用于我們寫的測試,另一個(gè)用于文檔測試。稍后我們將討論這一問題。現(xiàn)在,讓我們來看看這一行:
test it_works ... ok
注意 it_works。這是來自我們的函數(shù)的名稱:
fn it_works() {
我們還得到一個(gè)總結(jié):
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
那么為什么我們的測試能夠通過呢?任何非 panic 的測試都可以通過,任何 panic 的測試都會(huì)失敗。讓我們來看一個(gè)失敗的測試:
#[test]
fn it_works() {
assert!(false);
}
assert! 一種 Rust 提供的宏,它需要一個(gè)參數(shù):如果參數(shù)是true,什么也不會(huì)發(fā)生。如果參數(shù)是 false,它就成為 panic! 的。讓我們再次運(yùn)行我們的測試:
$ cargo test
Compiling adder v0.0.1 (file:///home/you/projects/adder)
Running target/adder-91b3e234d4ed382a
running 1 test
test it_works ... FAILED
failures:
---- it_works stdout ----
thread 'it_works' panicked at 'assertion failed: false', /home/steve/tmp/adder/src/lib.rs:3
failures:
it_works
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
thread '<main>' panicked at 'Some tests failed', /home/steve/src/rust/src/libtest/lib.rs:247
Rust 表明我們的測試失?。?/p>
test it_works ... FAILED
反映在結(jié)論中就是:
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
還可以得到一個(gè)非零的狀態(tài)代碼:
$ echo $?
101
如果你想將 cargo test 集成到其他工具,這是非常有用的。
我們可以用另一個(gè)屬性:should_panic 轉(zhuǎn)化我們的測試的失?。?/p>
#[test]
#[should_panic]
fn it_works() {
assert!(false);
}
如果我們 panic!,這個(gè)測試會(huì)成功,如果我們完成,則測試會(huì)失敗。讓我們來試一試:
$ cargo test
Compiling adder v0.0.1 (file:///home/you/projects/adder)
Running target/adder-91b3e234d4ed382a
running 1 test
test it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Rust 提供另一個(gè)宏 assert_eq!,用來比較兩個(gè)參數(shù)是否相等:
#[test]
#[should_panic]
fn it_works() {
assert_eq!("Hello", "world");
}
這個(gè)測試是否可以通過?因?yàn)榇嬖?should_panic 屬性,它可以通過:
$ cargo test
Compiling adder v0.0.1 (file:///home/you/projects/adder)
Running target/adder-91b3e234d4ed382a
running 1 test
test it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
should_panic 測試很脆弱。很難保證測試不會(huì)因?yàn)橐粋€(gè)意想不到的原因而失敗。為了解決這個(gè)問題,可以在 should_panic 屬性中添加一個(gè)可選的參數(shù):expected。測試工具將確保錯(cuò)誤消息包含提供的文本。上面示例的安全版本是:
#[test]
#[should_panic(expected = "assertion failed")]
fn it_works() {
assert_eq!("Hello", "world");
}
這就是所有的基礎(chǔ)讓我們來編寫一個(gè)“真正”的測試:
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[test]
fn it_works() {
assert_eq!(4, add_two(2));
}
這是 assert_eq! 的一個(gè)非常常見的用法:使用一些已知的參數(shù)調(diào)用某些函數(shù)并與預(yù)期的輸出比較。
有一種方式,以這種方式我們現(xiàn)有的例子都是不符合慣例的:它缺少測試模塊。我們的示例的慣用寫作方式,如下所示:
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::add_two;
#[test]
fn it_works() {
assert_eq!(4, add_two(2));
}
}
這里有一些變化。第一個(gè)是引入帶有 cfg 屬性的 mod tests。模塊允許我們對所有的測試進(jìn)行分組,如果需要也可以定義 helper 函數(shù),這個(gè)函數(shù)不會(huì)成為我們 crate 的一部分。如果目前我們試圖運(yùn)行這些代碼,cfg 屬性只會(huì)編譯我們的測試代碼。這可以節(jié)省編譯時(shí)間,也保證了我們構(gòu)建的測試是完全正常的。
第二個(gè)變化是 use 聲明。因?yàn)槲覀冊谝粋€(gè)內(nèi)部模塊中,我們需要將我們的測試函數(shù)設(shè)置范圍。如果你有一個(gè)大的模塊,這可能就會(huì)很惱人,所以這是 glob 屬性的一種常見的使用方式。讓我們改變我們的 src/lib.rs
以便能夠使用它:
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(4, add_two(2));
}
}
注意 use 行的不同使用方式?,F(xiàn)在,我們來運(yùn)行我們的測試:
$ cargo test
Updating registry `https://github.com/rust-lang/crates.io-index`
Compiling adder v0.0.1 (file:///home/you/projects/adder)
Running target/adder-91b3e234d4ed382a
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
看,運(yùn)轉(zhuǎn)起來了!
當(dāng)前的慣例是使用測試模塊 “unit-style” 測試。任何只測試一個(gè)小功能都是有意義的。如果用 “integration-style” 測試替代會(huì)怎么樣呢?為此,我們引出了測試目錄。
為了編寫集成測試,讓我們做一個(gè)測試目錄,并把一個(gè) tests/lib.rs
文件放在里面,這是它的內(nèi)容:
extern crate adder;
#[test]
fn it_works() {
assert_eq!(4, adder::add_two(2));
}
這類似于我們之前的測試,但略有不同。在代碼頂部有一個(gè) extern crate adder。這是因?yàn)樵跍y試目錄里測試是一個(gè)完全獨(dú)立的箱,所以我們需要導(dǎo)入我們的函數(shù)庫。這也是為什么 tests 是一個(gè)編寫集成風(fēng)格測試的合適的地方:他們使用函數(shù)庫和其他消費(fèi)者。
讓我們運(yùn)行它們:
$ cargo test
Compiling adder v0.0.1 (file:///home/you/projects/adder)
Running target/adder-91b3e234d4ed382a
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Running target/lib-c18e7d3494509e74
running 1 test
test it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
現(xiàn)在我們有三個(gè)部分:我們之前的測試在運(yùn)行,現(xiàn)在這個(gè)新的也在運(yùn)行。
這就是所有的 tests 目錄。這里不需要測試模塊,因?yàn)檎露际菍W⒂跍y試的。
讓我們最后檢查一下第三部分:文檔測試。
沒有什么是比帶有示例的文檔更好的了。沒有什么是比不能真正工作的例子更糟的了,一直以來文檔編寫已經(jīng)改變了代碼習(xí)慣。為此,Rust 支持自動(dòng)運(yùn)行你的文檔中的示例。這里有一個(gè)完整的 src/lib.rs
的例子:
//! The `adder` crate provides functions that add numbers to other numbers.
//!
//! # Examples
//!
//! ```
//! assert_eq!(4, adder::add_two(2));
//! ```
/// This function adds two to its argument.
///
/// # Examples
///
/// ```
/// use adder::add_two;
///
/// assert_eq!(4, add_two(2));
/// ```
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(4, add_two(2));
}
}
注意:模塊級(jí)文檔使用 //!
,函數(shù)文檔使用 ///
。Rust 的文檔支持 Markdown 中評(píng)論,所以三重斜線標(biāo)志代碼塊。包含 # Examples 部分是一種慣例,以下所示。
讓我們再次運(yùn)行測試:
$ cargo test
Compiling adder v0.0.1 (file:///home/steve/tmp/adder)
Running target/adder-91b3e234d4ed382a
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Running target/lib-c18e7d3494509e74
running 1 test
test it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests adder
running 2 tests
test add_two_0 ... ok
test _0 ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
現(xiàn)在我們運(yùn)行了所有三種測試!注意這些測試文檔的名稱:the_0 生成模塊測試,add_two_0 生成功能測試。當(dāng)你添加更多的例子,這些名字會(huì)自動(dòng)增量(例如 add_two_1)。