ch11-03-test-organization.md
commit cfb2c3cce7c20d4ad523dafdbf90ae3b25b1ba2c
本章一開始就提到,測試是一個(gè)復(fù)雜的概念,而且不同的開發(fā)者也采用不同的技術(shù)和組織。Rust 社區(qū)傾向于根據(jù)測試的兩個(gè)主要分類來考慮問題:單元測試(unit tests)與 集成測試(integration tests)。單元測試傾向于更小而更集中,在隔離的環(huán)境中一次測試一個(gè)模塊,或者是測試私有接口。而集成測試對(duì)于你的庫來說則完全是外部的。它們與其他外部代碼一樣,通過相同的方式使用你的代碼,只測試公有接口而且每個(gè)測試都有可能會(huì)測試多個(gè)模塊。
為了保證你的庫能夠按照你的預(yù)期運(yùn)行,從獨(dú)立和整體的角度編寫這兩類測試都是非常重要的。
單元測試的目的是在與其他部分隔離的環(huán)境中測試每一個(gè)單元的代碼,以便于快速而準(zhǔn)確的某個(gè)單元的代碼功能是否符合預(yù)期。單元測試與他們要測試的代碼共同存放在位于 src 目錄下相同的文件中。規(guī)范是在每個(gè)文件中創(chuàng)建包含測試函數(shù)的 tests
模塊,并使用 cfg(test)
標(biāo)注模塊。
測試模塊的 #[cfg(test)]
注解告訴 Rust 只在執(zhí)行 cargo test
時(shí)才編譯和運(yùn)行測試代碼,而在運(yùn)行 cargo build
時(shí)不這么做。這在只希望構(gòu)建庫的時(shí)候可以節(jié)省編譯時(shí)間,并且因?yàn)樗鼈儾]有包含測試,所以能減少編譯產(chǎn)生的文件的大小。與之對(duì)應(yīng)的集成測試因?yàn)槲挥诹硪粋€(gè)文件夾,所以它們并不需要 #[cfg(test)]
注解。然而單元測試位于與源碼相同的文件中,所以你需要使用 #[cfg(test)]
來指定他們不應(yīng)該被包含進(jìn)編譯結(jié)果中。
回憶本章第一部分新建的 adder
項(xiàng)目,Cargo 為我們生成了如下代碼:
文件名: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
上述代碼就是自動(dòng)生成的測試模塊。cfg
屬性代表 configuration ,它告訴 Rust 其之后的項(xiàng)只應(yīng)該被包含進(jìn)特定配置選項(xiàng)中。在這個(gè)例子中,配置選項(xiàng)是 test
,即 Rust 所提供的用于編譯和運(yùn)行測試的配置選項(xiàng)。通過使用 cfg
屬性,Cargo 只會(huì)在我們主動(dòng)使用 cargo test
運(yùn)行測試時(shí)才編譯測試代碼。這包括測試模塊中可能存在的幫助函數(shù), 以及標(biāo)注為 #[test] 的函數(shù)。
測試社區(qū)中一直存在關(guān)于是否應(yīng)該對(duì)私有函數(shù)直接進(jìn)行測試的論戰(zhàn),而在其他語言中想要測試私有函數(shù)是一件困難的,甚至是不可能的事。不過無論你堅(jiān)持哪種測試意識(shí)形態(tài),Rust 的私有性規(guī)則確實(shí)允許你測試私有函數(shù)??紤]示例 11-12 中帶有私有函數(shù) internal_adder
的代碼:
文件名: src/lib.rs
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}
fn internal_adder(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}
示例 11-12:測試私有函數(shù)
注意 internal_adder
函數(shù)并沒有標(biāo)記為 pub
。測試也不過是 Rust 代碼,同時(shí) tests
也僅僅是另一個(gè)模塊。正如 “路徑用于引用模塊樹中的項(xiàng)” 部分所說,子模塊的項(xiàng)可以使用其上級(jí)模塊的項(xiàng)。在測試中,我們通過 use super::*
將 test
模塊的父模塊的所有項(xiàng)引入了作用域,接著測試調(diào)用了 internal_adder
。如果你并不認(rèn)為應(yīng)該測試私有函數(shù),Rust 也不會(huì)強(qiáng)迫你這么做。
在 Rust 中,集成測試對(duì)于你需要測試的庫來說完全是外部的。同其他使用庫的代碼一樣使用庫文件,也就是說它們只能調(diào)用一部分庫中的公有 API 。集成測試的目的是測試庫的多個(gè)部分能否一起正常工作。一些單獨(dú)能正確運(yùn)行的代碼單元集成在一起也可能會(huì)出現(xiàn)問題,所以集成測試的覆蓋率也是很重要的。為了創(chuàng)建集成測試,你需要先創(chuàng)建一個(gè) tests 目錄。
為了編寫集成測試,需要在項(xiàng)目根目錄創(chuàng)建一個(gè) tests 目錄,與 src 同級(jí)。Cargo 知道如何去尋找這個(gè)目錄中的集成測試文件。接著可以隨意在這個(gè)目錄中創(chuàng)建任意多的測試文件,Cargo 會(huì)將每一個(gè)文件當(dāng)作單獨(dú)的 crate 來編譯。
讓我們來創(chuàng)建一個(gè)集成測試。保留示例 11-12 中 src/lib.rs 的代碼。創(chuàng)建一個(gè) tests 目錄,新建一個(gè)文件 tests/integration_test.rs,并輸入示例 11-13 中的代碼。
文件名: tests/integration_test.rs
use adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
示例 11-13:一個(gè) adder
crate 中函數(shù)的集成測試
與單元測試不同,我們需要在文件頂部添加 use adder
。這是因?yàn)槊恳粋€(gè) tests
目錄中的測試文件都是完全獨(dú)立的 crate,所以需要在每一個(gè)文件中導(dǎo)入庫。
并不需要將 tests/integration_test.rs 中的任何代碼標(biāo)注為 #[cfg(test)]
。 tests
文件夾在 Cargo 中是一個(gè)特殊的文件夾, Cargo 只會(huì)在運(yùn)行 cargo test
時(shí)編譯這個(gè)目錄中的文件?,F(xiàn)在就運(yùn)行 cargo test
試試:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 1.31s
Running unittests (target/debug/deps/adder-1082c4b063a8fbe6)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
現(xiàn)在有了三個(gè)部分的輸出:單元測試、集成測試和文檔測試。第一部分單元測試與我們之前見過的一樣:每個(gè)單元測試一行(示例 11-12 中有一個(gè)叫做 internal
的測試),接著是一個(gè)單元測試的摘要行。
集成測試部分以行 Running target/debug/deps/integration-test-ce99bcc2479f4607
(在輸出最后的哈希值可能不同)開頭。接下來每一行是一個(gè)集成測試中的測試函數(shù),以及一個(gè)位于 Doc-tests adder
部分之前的集成測試的摘要行。
我們已經(jīng)知道,單元測試函數(shù)越多,單元測試部分的結(jié)果行就會(huì)越多。同樣的,在集成文件中增加的測試函數(shù)越多,也會(huì)在對(duì)應(yīng)的測試結(jié)果部分增加越多的結(jié)果行。每一個(gè)集成測試文件有對(duì)應(yīng)的測試結(jié)果部分,所以如果在 tests 目錄中增加更多文件,測試結(jié)果中就會(huì)有更多集成測試結(jié)果部分。
我們?nèi)匀豢梢酝ㄟ^指定測試函數(shù)的名稱作為 cargo test
的參數(shù)來運(yùn)行特定集成測試。也可以使用 cargo test
的 --test
后跟文件的名稱來運(yùn)行某個(gè)特定集成測試文件中的所有測試:
$ cargo test --test integration_test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.64s
Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
這個(gè)命令只運(yùn)行了 tests 目錄中我們指定的文件 integration_test.rs
中的測試。
隨著集成測試的增加,你可能希望在 tests
目錄增加更多文件以便更好的組織他們,例如根據(jù)測試的功能來將測試分組。正如我們之前提到的,每一個(gè) tests 目錄中的文件都被編譯為單獨(dú)的 crate。
將每個(gè)集成測試文件當(dāng)作其自己的 crate 來對(duì)待,這更有助于創(chuàng)建單獨(dú)的作用域,這種單獨(dú)的作用域能提供更類似與最終使用者使用 crate 的環(huán)境。然而,正如你在第七章中學(xué)習(xí)的如何將代碼分為模塊和文件的知識(shí),tests 目錄中的文件不能像 src 中的文件那樣共享相同的行為。
當(dāng)你有一些在多個(gè)集成測試文件都會(huì)用到的幫助函數(shù),而你嘗試按照第七章 “將模塊移動(dòng)到其他文件” 部分的步驟將他們提取到一個(gè)通用的模塊中時(shí), tests 目錄中不同文件的行為就會(huì)顯得很明顯。例如,如果我們可以創(chuàng)建 一個(gè)tests/common.rs 文件并創(chuàng)建一個(gè)名叫 setup
的函數(shù),我們希望這個(gè)函數(shù)能被多個(gè)測試文件的測試函數(shù)調(diào)用:
文件名: tests/common.rs
pub fn setup() {
// setup code specific to your library's tests would go here
}
如果再次運(yùn)行測試,將會(huì)在測試結(jié)果中看到一個(gè)新的對(duì)應(yīng) common.rs 文件的測試結(jié)果部分,即便這個(gè)文件并沒有包含任何測試函數(shù),也沒有任何地方調(diào)用了 setup
函數(shù):
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.89s
Running unittests (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
我們并不想要common
出現(xiàn)在測試結(jié)果中顯示 running 0 tests
。我們只是希望其能被其他多個(gè)集成測試文件中調(diào)用罷了。
為了不讓 common
出現(xiàn)在測試輸出中,我們將創(chuàng)建 tests/common/mod.rs ,而不是創(chuàng)建 tests/common.rs 。這是一種 Rust 的命名規(guī)范,這樣命名告訴 Rust 不要將 common
看作一個(gè)集成測試文件。將 setup
函數(shù)代碼移動(dòng)到 tests/common/mod.rs 并刪除 tests/common.rs 文件之后,測試輸出中將不會(huì)出現(xiàn)這一部分。tests 目錄中的子目錄不會(huì)被作為單獨(dú)的 crate 編譯或作為一個(gè)測試結(jié)果部分出現(xiàn)在測試輸出中。
一旦擁有了 tests/common/mod.rs,就可以將其作為模塊以便在任何集成測試文件中使用。這里是一個(gè) tests/integration_test.rs 中調(diào)用 setup
函數(shù)的 it_adds_two
測試的例子:
文件名: tests/integration_test.rs
use adder;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}
注意 mod common;
聲明與示例 7-21 中展示的模塊聲明相同。接著在測試函數(shù)中就可以調(diào)用 common::setup()
了。
如果項(xiàng)目是二進(jìn)制 crate 并且只包含 src/main.rs 而沒有 src/lib.rs,這樣就不可能在 tests 目錄創(chuàng)建集成測試并使用 extern crate
導(dǎo)入 src/main.rs 中定義的函數(shù)。只有庫 crate 才會(huì)向其他 crate 暴露了可供調(diào)用和使用的函數(shù);二進(jìn)制 crate 只意在單獨(dú)運(yùn)行。
為什么 Rust 二進(jìn)制項(xiàng)目的結(jié)構(gòu)明確采用 src/main.rs 調(diào)用 src/lib.rs 中的邏輯的方式?因?yàn)橥ㄟ^這種結(jié)構(gòu),集成測試 就可以 通過 extern crate
測試庫 crate 中的主要功能了,而如果這些重要的功能沒有問題的話,src/main.rs 中的少量代碼也就會(huì)正常工作且不需要測試。
Rust 的測試功能提供了一個(gè)確保即使你改變了函數(shù)的實(shí)現(xiàn)方式,也能繼續(xù)以期望的方式運(yùn)行的途徑。單元測試獨(dú)立地驗(yàn)證庫的不同部分,也能夠測試私有函數(shù)實(shí)現(xiàn)細(xì)節(jié)。集成測試則檢查多個(gè)部分是否能結(jié)合起來正確地工作,并像其他外部代碼那樣測試庫的公有 API。即使 Rust 的類型系統(tǒng)和所有權(quán)規(guī)則可以幫助避免一些 bug,不過測試對(duì)于減少代碼中不符合期望行為的邏輯 bug 仍然是很重要的。
讓我們將本章和其他之前章節(jié)所學(xué)的知識(shí)組合起來,在下一章一起編寫一個(gè)項(xiàng)目!
更多建議: