> 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代碼的正確方法。這里有很多關于寫測試好壞方法的流派。所有的這些途徑都使用相同的基本工具,所以我們會想你展示他們的語法。
## `測試`屬性(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的狀態值:
~~~
$ echo $?
101
~~~
這在你想把`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!`是非常常見的;用已知的參數調用一些函數然后與期望的輸出進行比較。
# `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 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
~~~
目前的習慣是使用`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 test::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支持自動運行你文檔中的例子。這是一個完整的有例子的`src/lib.rs`:
~~~
//! The `adder` crate provides functions that add numbers to other numbers.
//!
//! # Examples
//!
//! ` ` ` 因為gitbook排版問題,這里多寫了兩個空格
//! assert_eq!(4, adder::add_two(2));
//! ` ` ` 因為gitbook排版問題,這里多寫了兩個空格
/// This function adds two to its argument.
///
/// # Examples
///
/// ` ` ` 因為gitbook排版問題,這里多寫了兩個空格
/// use adder::add_two;
///
/// assert_eq!(4, add_two(2));
/// ` ` ` 因為gitbook排版問題,這里多寫了兩個空格
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 test::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`這樣自動加一。
- 前言
- 1.介紹
- 2.準備
- 2.1.安裝Rust
- 2.2.Hello, world!
- 2.3.Hello, Cargo!
- 3.學習Rust
- 3.1.猜猜看
- 3.2.哲學家就餐問題
- 3.3.其它語言中的Rust
- 4.高效Rust
- 4.1.棧和堆
- 4.2.測試
- 4.3.條件編譯
- 4.4.文檔
- 4.5.迭代器
- 4.6.并發
- 4.7.錯誤處理
- 4.8.外部語言接口
- 4.9.Borrow 和 AsRef
- 4.10.發布途徑
- 5.語法和語義
- 5.1.變量綁定
- 5.2.函數
- 5.3.原生類型
- 5.4.注釋
- 5.5.If語句
- 5.6.for循環
- 5.7.while循環
- 5.8.所有權
- 5.9.引用和借用
- 5.10.生命周期
- 5.11.可變性
- 5.12.結構體
- 5.13.枚舉
- 5.14.匹配
- 5.15.模式
- 5.16.方法語法
- 5.17.Vectors
- 5.18.字符串
- 5.19.泛型
- 5.20.Traits
- 5.21.Drop
- 5.22.if let
- 5.23.trait對象
- 5.24.閉包
- 5.25.通用函數調用語法
- 5.26.包裝箱和模塊
- 5.27.`const`和`static`
- 5.28.屬性
- 5.29.`type`別名
- 5.30.類型轉換
- 5.31.關聯類型
- 5.32.不定長類型
- 5.33.運算符和重載
- 5.34.`Deref`強制多態
- 5.35.宏
- 5.36.裸指針
- 6.Rust開發版
- 6.1.編譯器插件
- 6.2.內聯匯編
- 6.3.不使用標準庫
- 6.4.固有功能
- 6.5.語言項
- 6.6.鏈接參數
- 6.7.基準測試
- 6.8.裝箱語法和模式
- 6.9.切片模式
- 6.10.關聯常量
- 7.詞匯表
- 8.學院派研究
- 勘誤