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