俗話說:“測試寫得好,獎金少不了。”
有經驗的開發人員通常會通過單元測試來保證代碼基本邏輯的正確性。如果你是一名新手開發者,並且還沒體會到單元測試的好處,那麼建議你先讀一下我之前的一篇文章代碼潔癖系列(七):單元測試的地位。
寫單元測試一般需要三個步驟:
- 準備測試用例,測試用例要能覆蓋儘可能多的代碼
- 執行需要測試的代碼
- 判斷結果,是否是你希望得到的結果
瞭解了這些以後,我們就來看看在Rust中應該怎麼寫單元測試。
首先我們建立一個library項目
$ cargo new adder --lib
Created library `adder` project
然後在src/lib.rs文件中開始寫測試代碼
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
此時在命令行運行cargo test
就會得到測試結果
可以看到,結果顯示,Rust運行了一項測試並且測試通過。後面的Doc-tests我們先放下,以後再聊。
當然,這並不是我們常見的測試,在日常開發中,我們通常是先寫我們的業務代碼然後再對各個函數進行單元測試,最後還會對某個模塊進行集成測試。那麼我們就來模擬一下日常開發過程中應該如何來寫測試。
單元測試
我們仍然是用上面的項目,先來在src/lib.rs中寫一段“業務代碼”
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}
fn internal_adder(a: i32, b: i32) -> i32 {
a + b
}
這是一段非常簡單的代碼,對外暴露的函數只是一個加2的功能,內部調用了一個兩數相加的函數。現在我們就對這個內部函數做一個單元測試。
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}
在測試模塊中,如果想要使用我們業務代碼中的函數,就需要通過use super::*;
將其引入可用範圍。接着,還是執行cargo test
,測試結果與剛纔類似。
測了半天全是通過的沒什麼意思,單元測試真正的作用是要發現代碼中的問題,所以我們來嘗試一個錯誤的試一下。假設我們希望2+2等於5。
這裏我們的assert_eq!左右不相等,引起了線程恐慌,因此導致測試失敗。結果中給出了失敗的原因,引起失敗的位置,並且有一句提示:note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
我們按照這個提示,設置變量RUST_BACKTRACE=1,此時再執行cargo test
。
Rust就會將錯誤棧打印出來,根據結果提示,這並不是完整的錯誤棧,我們還可以將RUST_BACKTRACE設置爲full來查看更加詳細的信息。這裏我就不做演示了。
集成測試
接下來我們再演示一下集成測試。我們通常將集成測試單獨放到一個目錄中,在lib.rs文件中,rust識別測試mod的名稱是tests,同樣的,我們在src下創建tests目錄。tests目錄下就是我們的所有集成測試代碼。
如圖,integration_test是我們測試代碼的文件,common目錄下的mod.rs文件中是一些集成測試必要的配置。這裏我們只是放了一個空的setup函數。
在集成測試中,我們就要像正常他人使用我們的代碼時那樣來進行測試,首先需要將我們的mod引入到可用範圍,當然還需要加上common的mod。
use adder;
mod common;
#[tests]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}
接着就可以測試我們對外暴露的函數了。
ok,集成測試的方法我們也掌握了。現在來看看一直被我們忽略的Doc-tests吧。
文檔測試
我們已經知道,Rust中的註釋是雙斜線//
,像我們剛剛寫的library代碼,如果想要把它發佈到crate.io上讓別人使用,那麼我們就需要增加相應的文檔,這裏文檔的每行都應該是三斜線///
開頭,而文檔中也應該放一些例子供他人蔘考。
/// Adds two to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = adder::add_two(arg);
///
/// assert_eq!(7, answer);
/// ```
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}
現在我給add_two函數加上了文檔,我們再次執行cargo test
命令。
現在我們就明白了,Doc-tests測試就是運行我們文檔中的例子。
常用特性
到目前爲止,我們已經知道了在Rust中如何寫測試代碼了。接下來我們再來了解幾個比較常用的特性。
運行指定的測試代碼
我們在開發過程中肯定不會每次都去跑全量的單元測試,那樣太浪費時間了。通常是我們開發完一個功能之後,編寫對應的單元測試,然後單獨跑這個測試。那麼Rust中能不能單獨跑一個單元測試呢?答案是肯定的。
相信細心的同學已經發現了,Rust測試結果中,是針對每個測試單獨統計結果,並且每個測試都有自己的名字,像我們前面寫的it_works
和internal
。假設我們的代碼中同時存在這兩個函數,如果你想要單獨跑internal這一個測試,就可以使用cargo test internal
命令。
你也可以使用這種方法來執行多個名稱類似的測試,假如我們有名稱爲internal_a
的測試,那麼執行cargo test internal
命令時它也會被執行。
忽略某個測試
當我們有一個測試執行時間非常長的時候,我們一般不會輕易去執行,這時如果你想要執行多個測試,除了用我們上面提到的方法,去指定不同的名稱列表以外。還可以把這個測試忽略掉。
現在我不想執行internal
測試了,只需要對代碼進行如下改動:
#[test]
#[ignore]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
這時再來運行測試,結果如圖所示。
我們發現此時internal
測試已經被忽略了。
測試異常情況
除了測試代碼邏輯正常的情況,我們有時還需要測試一些異常情況,比如接收到非法參數時程序能否返回我們希望看到的異常。
我們首先來看一下如何測試程序返回異常信息。
Rust爲我們提供了一個叫做should_panic的註解。我們可以使用它來測試程序是否返回異常:
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}
fn internal_adder(a: i32, b: i32) -> i32 {
if a < 0 {
panic!("a should bigger than 0");
}
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn internal() {
assert_eq!(4, internal_adder(-2, 2));
}
}
此時我們運行測試時就會發現internal測試通過,因爲它發生了線程恐慌,這是我們希望看到的結果。
另外,我們還可以再指定我們具體期望的異常,那麼就可以在should_panic後面加上expected參數。
#[test]
#[should_panic(expected = "a should be positive")]
fn internal() {
assert_eq!(4, internal_adder(-2, 2));
}
大家可以自行運行一下這段測試代碼看看效果。
總結
文中我向大家介紹了在Rust中如何進行單元測試、集成測試,還有比較特殊的文檔測試。最後還介紹了3種常見的測試特性。
最後想友情提醒大家一下,在開發過程中,不要寫完一堆功能後再開始寫單元測試,這時你很有可能會因爲測試代碼過於繁瑣而放棄。建議大家每寫一個功能,隨即開始進行單元測試,這樣也能立即看到自己的代碼的執行效果,提高成就感。這就是所謂的“步步爲營”。