測試
程序測試是一種找到缺陷的有效方式,但是它對證明沒有缺陷卻無能為力。
Edsger W. Dijkstra, "The Humble Programmer" (1972)
作為軟件工程質量保障體系的重要一環,測試是應該引起我們充分注意並重視的事情。前面說過,Rust 語言的設計集成了最近十多年中總結出來的大量最佳工程實踐,而對測試的原生集成也正體現了這一點。下面來看 Rust 是怎麼設計測試特性的。
Rust 的測試特性按精細度劃分,分為 3 個層次:
- 函數級;
- 模塊級;
- 工程級;
另外,Rust 還支持對文檔進行測試。
函數級測試
在本章中,我們用創建一個庫的實操來講解測試的內容。我們先用 cargo 建立一個庫工程:adder
$ cargo new adder
$ cd adder
#[test]
標識
打開 src/lib.rs
文件,可以看到如下代碼
#[test]
fn it_works() {
// do test work
}
Rust 中,只需要在一個函數的上面,加上 #[test]
就標明這是一個測試用的函數。
有了這個屬性之後,在使用 cargo build
編譯時,就會忽略這些函數。使用 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
Rust 提供了兩個宏來執行測試斷言:
assert!(expr) 測試表達式是否為 true 或 false
assert_eq!(expr, expr) 測試兩個表達式的結果是否相等
比如
#[test]
fn it_works() {
assert!(false);
}
運行 cargo test
,你會得到類似下面這樣的提示
$ 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
#[should_panic]
標識
如果你的測試函數沒完成,或沒有更新,或是故意讓它崩潰,但為了讓測試能夠順利完成,我們主動可以給測試函數加上 #[should_panic]
標識,就不會讓 cargo test
報錯了。
如
#[test]
#[should_panic]
fn it_works() {
assert!(false);
}
運行 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
#[ignore]
標識
有時候,某個測試函數非常耗時,或暫時沒更新,我們想不讓它參與測試,但是又不想刪除它,這時, #[ignore]
就派上用場了。
#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
寫上這個,運行 cargo test
的時候,就不會測試這個函數。
模塊級測試
有時,我們會組織一批測試用例,這時,模塊化的組織結構就有助於建立結構性的測試體系。Rust 中,可以類似如下寫法:
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));
}
}
也即在 mod
的上面寫上 #[cfg(test)]
,表明這個模塊是個測試模塊。一個測試模塊中,可以包含若干測試函數,測試模塊中還可以繼續包含測試模塊,即模塊的嵌套。
如此,就形式了結構化的測試體系,甚是方便。
工程級測試
函數級和模塊級的測試,代碼是與要測試的模塊(編譯單元)寫在相同的文件中,一般做的是白盒測試。工程級的測試,一般做的就是黑盒集成測試了。
我們看一個工程的目錄,在這個目錄下,有一個 tests
文件夾(沒有的話,就手動建立)
Cargo.toml
Cargo.lock
examples
src
tests
我們在 tests 目錄下,建立一個文件 testit.rs
,名字隨便取皆可。內容為:
extern crate adder;
#[test]
fn it_works() {
assert_eq!(4, adder::add_two(2));
}
這裡,比如,我們 src 中,寫了一個庫,提供了一個 add_two
函數,現在進行集成測試。
首先,用 extern crate
的方式,引入這個庫,由於是同一個項目,cargo 會自動找。引入後,就按模塊的使用方法調用就行了,其它的測試標識與前面相同。
寫完後,運行一下 cargo test
,提示類似如下:
$ 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
文檔級測試
Rust 對文檔的哲學,是不要單獨寫文檔,一是代碼本身是文檔,二是代碼的註釋就是文檔。Rust 不但可以自動抽取代碼中的文檔,形成標準形式的文檔集合,還可以對文檔中的示例代碼進行測試。
比如,我們給上面庫加點文檔:
//! 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));
}
}
運行 cargo test
,結果如下:
$ 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
看到了吧,多了些測試結果。
結語
我們可以看到,Rust 對測試,對文檔,對文檔中的示例代碼測試,都有特性支持。從這些細節之處,可以看出 Rust 設計的周密性和嚴謹性。
但是,光有好工具是不夠的,工程的質量更重要的是寫代碼的人決定的。我們應該在 Rust 嚴謹之風的薰陶下,養成良好的編碼和編寫測試的習慣,掌握一定的分析方法,把質量要求貫徹到底。