Rust 測試的組織結(jié)構(gòu)

2023-03-22 15:10 更新
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)]

測試模塊的 #[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ù)。

測試私有函數(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 目錄。

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() 了。

二進(jìn)制 crate 的集成測試

如果項(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ì)正常工作且不需要測試。

總結(jié)

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)目!


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)