# 測試
> [testing.md](https://github.com/rust-lang/rust/blob/master/src/doc/book/testing.md)
commit 6ba952020fbc91bad64be1ea0650bfba52e6aab4
> Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.
> Edsger W. Dijkstra, "The Humble Programmer" (1972)
> 軟件測試是證明bug存在的有效方法,而證明它們不存在時則顯得令人絕望的不足。
> Edsger W. Dijkstra,【謙卑的程序員】(1972)
讓我們討論一下如何測試Rust代碼。在這里我們不會討論什么是測試Rust代碼的正確方法。有很多關于寫測試好壞方法的流派。所有的這些途徑都使用相同的基本工具,所以我們會向你展示他們的語法。
### `test`屬性(The test attribute)
簡單的說,測試是一個標記為`test`屬性的函數。讓我們用 Cargo 來創建一個叫`adder`的項目:
~~~
$ cargo new adder
$ cd adder
~~~
在你創建一個新項目時 Cargo 會自動生成一個簡單的測試。下面是`src/lib.rs`的內容:
~~~
#[test]
fn it_works() {
}
~~~
注意這個`#[test]`。這個屬性表明這是一個測試函數。它現在沒有函數體。它肯定能編譯通過!讓我們用`cargo test`運行測試:
~~~
$ 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 編譯和運行了我們的測試。這里有兩部分輸出:一個是我們寫的測試,另一個是文檔測試。我們稍后再討論這些。現在,看看這行:
~~~
test it_works ... ok
~~~
注意那個`it_works`。這是我們函數的名字:
~~~
fn it_works() {
# }
~~~
然后我們有一個總結行:
~~~
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
~~~
那么為啥我們這個啥都沒干的測試通過了呢?任何沒有`panic!`的測試通過,`panic!`的測試失敗。讓我們的測試失敗:
~~~
#[test]
fn it_works() {
assert!(false);
}
~~~
`assert!`是Rust提供的一個宏,它接受一個參數:如果參數是`true`,啥也不會發生。如果參數是`false`,它會`panic!`。讓我們再次運行我們的測試:
~~~
$ 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指出我們的測試失敗了:
~~~
test it_works ... FAILED
~~~
這反映在了總結行上:
~~~
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
~~~
我們也得到了一個非0的狀態碼.我們在 OS X和 Linux 中使用`$?`:
~~~
$ echo $?
101
~~~
在 Windows 中,如果你使用`cmd`:
~~~
> echo %ERRORLEVEL%
~~~
而如果你使用 PowerShell:
~~~
> echo $LASTEXITCODE # the code itself
> echo $? # a boolean, fail or succeed
~~~
這在你想把`cargo test`集成進其它工具時是非常有用。
我們可以使用另一個屬性反轉我們的失敗的測試:`should_panic`:
~~~
#[test]
#[should_panic]
fn it_works() {
assert!(false);
}
~~~
現在即使我們`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
~~~
Rust提供了另一個宏,`assert_eq!`用來比較兩個參數:
~~~
#[test]
#[should_panic]
fn it_works() {
assert_eq!("Hello", "world");
}
~~~
那個測試通過了嗎?因為那個`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`測試是脆弱的,因為很難保證測試是否會因什么不可預測原因并未失敗。為了解決這個問題,`should_panic`屬性可以添加一個可選的`expected`參數。這個參數可以確保失敗信息中包含我們提供的文字。下面是我們例子的一個更安全的版本:
~~~
#[test]
#[should_panic(expected = "assertion failed")]
fn it_works() {
assert_eq!("Hello", "world");
}
~~~
這就是全部的基礎內容!讓我們寫一個“真實”的測試:
~~~
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[test]
fn it_works() {
assert_eq!(4, add_two(2));
}
~~~
`assert_eq!`是非常常見的;用已知的參數調用一些函數然后與期望的輸出進行比較。
### `ignore`屬性
有時一些特定的測試可能非常耗時。這時可以通過`ignore`屬性來默認禁用:
~~~
#[test]
fn it_works() {
assert_eq!(4, add_two(2));
}
#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
~~~
現在我們運行測試并發現`it_works`被執行了,而`expensive_test`沒有
~~~
$ cargo test
Compiling adder v0.0.1 (file:///home/you/projects/adder)
Running target/adder-91b3e234d4ed382a
running 2 tests
test expensive_test ... ignored
test it_works ... ok
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
~~~
耗時的測試可以通過調用`cargo test -- --ignored`來執行:
~~~
$ cargo test -- --ignored
Running target/adder-91b3e234d4ed382a
running 1 test
test expensive_test ... 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
~~~
`--ignored`參數是 test 程序的參數,而不是 Cargo 的,這也是為什么命令是`cargo test -- --ignored`。
### `tests`模塊
然而以這樣的方式來實現我們的測試的例子并不是地道的做法:它缺少`tests`模塊。如果要實現我們的測試實例,一個比較慣用的做法應該是如下的:
~~~
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));
}
}
~~~
這里產生了一些變化。第一個變化是引入了一個`cfg`屬性的`mod tests`。這個模塊允許我們把所有測試集中到一起,并且需要的話還可以定義輔助函數,它們不會成為我們包裝箱的一部分。`cfg`屬性只會在我們嘗試去運行測試時才會編譯測試代碼。這樣可以節省編譯時間,并且也確保我們的測試代碼完全不會出現在我們的正式構建中。
第二個變化是`use`聲明。因為我們在一個內部模塊中,我們需要把我們要測試的函數導入到當前空間中。如果你有一個大型模塊的話這會非常煩人,所以這里有經常使用一個`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`行的變化。現在運行我們的測試:
~~~
$ 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
~~~
它能工作了!
目前的習慣是使用`test`模塊來存放你的“單元測試”。任何只是測試一小部分功能的測試理應放在這里。那么“集成測試”怎么辦呢?我們有`tests`目錄來處理這些。
### `tests`目錄
為了進行集成測試,讓我們創建一個`tests`目錄,然后放一個`tests/lib.rs`文件進去,輸入如下內容:
~~~
extern crate adder;
#[test]
fn it_works() {
assert_eq!(4, adder::add_two(2));
}
~~~
這看起來與我們剛才的測試很像,不過有些許的不同。我們現在有一行`extern crate adder`在開頭。這是因為在`tests`目錄中的測試另一個完全不同的包裝箱,所以我們需要導入我們的庫。這也是為什么`tests`是一個寫集成測試的好地方:它們就像其它程序一樣使用我們的庫。
讓我們運行一下:
~~~
$ 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
~~~
現在我們有了三個部分:我們之前的兩個測試,然后還有我們新添加的。
這就是`tests`目錄的全部內容。它不需要`test`模塊因為它整個就是關于測試的。
讓我們最后看看第三部分:文檔測試。
### 文檔測試
沒有什么是比帶有例子的文檔更好的了。當然也沒有什么比不能工作的例子更糟的,因為文檔完成之后代碼已經被改寫。為此,Rust支持自動運行你文檔中的例子(**注意:**這只在庫 crate中有用,而在二進制 crate 中沒用)。這是一個完整的有例子的`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));
}
}
~~~
注意模塊級的文檔以`//!`開頭然后函數級的文檔以`///`開頭。Rust文檔在注釋中支持Markdown語法,所以它支持3個反單引號代碼塊語法。想上面例子那樣,加入一個`# Examples`部分被認為是一個慣例。
讓我們再次運行測試:
~~~
$ 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
~~~
現在我們運行了3種測試!注意文檔測試的名稱:`_0`生成為模塊測試,而`add_two_0`函數測試。如果你添加更多用例的話它們會像`add_two_1`這樣自動加一。
我們還沒有講到所有編寫文檔測試的所有細節。關于更多,請看[文檔章節](#)。
最后再強調一次:文檔測試不能在二進制 crate 中運行。關于文件編排的細節請看[crate 和模塊](#)部分。
- 前言
- 貢獻者
- 1.介紹
- 2.準備
- 3.學習 Rust
- 3.1.猜猜看
- 3.2.哲學家就餐問題
- 3.3.其它語言中的 Rust
- 4.語法和語義
- 4.1.變量綁定
- 4.2.函數
- 4.3.原生類型
- 4.4.注釋
- 4.5.If語句
- 4.6.循環
- 4.7.所有權
- 4.8.引用和借用
- 4.9.生命周期
- 4.10.可變性
- 4.11.結構體
- 4.12.枚舉
- 4.13.匹配
- 4.14.模式
- 4.15.方法語法
- 4.16.Vectors
- 4.17.字符串
- 4.18.泛型
- 4.19.Traits
- 4.20.Drop
- 4.21.if let
- 4.22.trait 對象
- 4.23.閉包
- 4.24.通用函數調用語法
- 4.25.crate 和模塊
- 4.26.const和static
- 4.27.屬性
- 4.28.type別名
- 4.29.類型轉換
- 4.30.關聯類型
- 4.31.不定長類型
- 4.32.運算符和重載
- 4.33.Deref強制多態
- 4.34.宏
- 4.35.裸指針
- 4.36.不安全代碼
- 5.高效 Rust
- 5.1.棧和堆
- 5.2.測試
- 5.3.條件編譯
- 5.4.文檔
- 5.5.迭代器
- 5.6.并發
- 5.7.錯誤處理
- 5.8.選擇你的保證
- 5.9.外部函數接口
- 5.10.Borrow 和 AsRef
- 5.11.發布途徑
- 5.12.不使用標準庫
- 6.Rust 開發版
- 6.1.編譯器插件
- 6.2.內聯匯編
- 6.4.固有功能
- 6.5.語言項
- 6.6.鏈接進階
- 6.7.基準測試
- 6.8.裝箱語法和模式
- 6.9.切片模式
- 6.10.關聯常量
- 6.11.自定義內存分配器
- 7.詞匯表
- 8.語法索引
- 9.參考文獻
- 附錄:名詞中英文對照