ch12-04-testing-the-librarys-functionality.md
commit 04170d1feee2a47525b39f1edce77ba615ca9cdf
現(xiàn)在我們將邏輯提取到了 src/lib.rs 并將所有的參數(shù)解析和錯誤處理留在了 src/main.rs 中,為代碼的核心功能編寫測試將更加容易。我們可以直接使用多種參數(shù)調(diào)用函數(shù)并檢查返回值而無需從命令行運行二進制文件了。
在這一部分,我們將遵循測試驅(qū)動開發(fā)(Test Driven Development, TDD)的模式來逐步增加 minigrep
的搜索邏輯。這是一個軟件開發(fā)技術,它遵循如下步驟:
這只是眾多編寫軟件的方法之一,不過 TDD 有助于驅(qū)動代碼的設計。在編寫能使測試通過的代碼之前編寫測試有助于在開發(fā)過程中保持高測試覆蓋率。
我們將測試驅(qū)動實現(xiàn)實際在文件內(nèi)容中搜索查詢字符串并返回匹配的行示例的功能。我們將在一個叫做 search
的函數(shù)中增加這些功能。
去掉 src/lib.rs 和 src/main.rs 中用于檢查程序行為的 println!
語句,因為不再真正需要他們了。接著我們會像 第十一章 那樣增加一個 test
模塊和一個測試函數(shù)。測試函數(shù)指定了 search
函數(shù)期望擁有的行為:它會獲取一個需要查詢的字符串和用來查詢的文本,并只會返回包含請求的文本行。示例 12-15 展示了這個測試,它還不能編譯:
文件名: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
示例 12-15:創(chuàng)建一個我們期望的 search
函數(shù)的失敗測試
這里選擇使用 "duct"
作為這個測試中需要搜索的字符串。用來搜索的文本有三行,其中只有一行包含 "duct"
。(注意雙引號之后的反斜杠,這告訴 Rust 不要在字符串字面值內(nèi)容的開頭加入換行符)我們斷言 search
函數(shù)的返回值只包含期望的那一行。
我們還不能運行這個測試并看到它失敗,因為它甚至都還不能編譯:search
函數(shù)還不存在呢!我們將增加足夠的代碼來使其能夠編譯:一個總是會返回空 vector 的 search
函數(shù)定義,如示例 12-16 所示。然后這個測試應該能夠編譯并因為空 vector 并不匹配一個包含一行 "safe, fast, productive."
的 vector 而失敗。
文件名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
示例 12-16:剛好足夠使測試通過編譯的 search
函數(shù)定義
注意需要在 search
的簽名中定義一個顯式生命周期 'a
并用于 contents
參數(shù)和返回值。回憶一下 第十章 中講到生命周期參數(shù)指定哪個參數(shù)的生命周期與返回值的生命周期相關聯(lián)。在這個例子中,我們表明返回的 vector 中應該包含引用參數(shù) contents
(而不是參數(shù)query
) slice 的字符串 slice。
換句話說,我們告訴 Rust 函數(shù) search
返回的數(shù)據(jù)將與 search
函數(shù)中的參數(shù) contents
的數(shù)據(jù)存在的一樣久。這是非常重要的!為了使這個引用有效那么 被 slice 引用的數(shù)據(jù)也需要保持有效;如果編譯器認為我們是在創(chuàng)建 query
而不是 contents
的字符串 slice,那么安全檢查將是不正確的。
如果嘗試不用生命周期編譯的話,我們將得到如下錯誤:
$ cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:28:51
|
28 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
|
28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` due to previous error
Rust 不可能知道我們需要的是哪一個參數(shù),所以需要告訴它。因為參數(shù) contents
包含了所有的文本而且我們希望返回匹配的那部分文本,所以我們知道 contents
是應該要使用生命周期語法來與返回值相關聯(lián)的參數(shù)。
其他語言中并不需要你在函數(shù)簽名中將參數(shù)與返回值相關聯(lián)。所以這么做可能仍然感覺有些陌生,隨著時間的推移這將會變得越來越容易。你可能想要將這個例子與第十章中 “生命周期與引用有效性” 部分做對比。
現(xiàn)在運行測試:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished test [unoptimized + debuginfo] target(s) in 0.97s
Running unittests (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::one_result ... FAILED
failures:
---- tests::one_result stdout ----
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `["safe, fast, productive."]`,
right: `[]`', src/lib.rs:44:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
好的,測試失敗了,這正是我們所期望的。修改代碼來讓測試通過吧!
目前測試之所以會失敗是因為我們總是返回一個空的 vector。為了修復并實現(xiàn) search
,我們的程序需要遵循如下步驟:
讓我們一步一步的來,從遍歷每行開始。
Rust 有一個有助于一行一行遍歷字符串的方法,出于方便它被命名為 lines
,它如示例 12-17 這樣工作。注意這還不能編譯:
文件名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// 對文本行進行操作
}
}
示例 12-17:遍歷 contents
的每一行
lines
方法返回一個迭代器。第十三章 會深入了解迭代器,不過我們已經(jīng)在 示例 3-5 中見過使用迭代器的方法了,在那里使用了一個 for
循環(huán)和迭代器在一個集合的每一項上運行了一些代碼。
接下來將會增加檢查當前行是否包含查詢字符串的功能。幸運的是,字符串類型為此也有一個叫做 contains
的實用方法!如示例 12-18 所示在 search
函數(shù)中加入 contains
方法調(diào)用。注意這仍然不能編譯:
文件名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(query) {
// 對文本行進行操作
}
}
}
示例 12-18:增加檢查文本行是否包含 query
中字符串的功能
我們還需要一個方法來存儲包含查詢字符串的行。為此可以在 for
循環(huán)之前創(chuàng)建一個可變的 vector 并調(diào)用 push
方法在 vector 中存放一個 line
。在 for
循環(huán)之后,返回這個 vector,如示例 12-19 所示:
文件名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
示例 12-19:儲存匹配的行以便可以返回他們
現(xiàn)在 search
函數(shù)應該返回只包含 query
的那些行,而測試應該會通過。讓我們運行測試:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished test [unoptimized + debuginfo] target(s) in 1.22s
Running unittests (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
測試通過了,它可以工作了!
現(xiàn)在正是可以考慮重構(gòu)的時機,在保證測試通過,保持功能不變的前提下重構(gòu) search
函數(shù)。search
函數(shù)中的代碼并不壞,不過并沒有利用迭代器的一些實用功能。第十三章將回到這個例子并深入探索迭代器并看看如何改進代碼。
現(xiàn)在 search
函數(shù)是可以工作并測試通過了的,我們需要實際在 run
函數(shù)中調(diào)用 search
。需要將 config.query
值和 run
從文件中讀取的 contents
傳遞給 search
函數(shù)。接著 run
會打印出 search
返回的每一行:
文件名: src/lib.rs
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
for line in search(&config.query, &contents) {
println!("{}", line);
}
Ok(())
}
這里仍然使用了 for
循環(huán)獲取了 search
返回的每一行并打印出來。
現(xiàn)在整個程序應該可以工作了!讓我們試一試,首先使用一個只會在艾米莉·狄金森的詩中返回一行的單詞 “frog”:
$ cargo run frog poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep frog poem.txt`
How public, like a frog
好的!現(xiàn)在試試一個會匹配多行的單詞,比如 “body”:
$ cargo run body poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!
最后,讓我們確保搜索一個在詩中哪里都沒有的單詞時不會得到任何行,比如 "monomorphization":
$ cargo run monomorphization poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep monomorphization poem.txt`
非常好!我們創(chuàng)建了一個屬于自己的迷你版經(jīng)典工具,并學習了很多如何組織程序的知識。我們還學習了一些文件輸入輸出、生命周期、測試和命令行解析的內(nèi)容。
為了使這個項目更豐滿,我們將簡要的展示如何處理環(huán)境變量和打印到標準錯誤,這兩者在編寫命令行程序時都很有用。
更多建議: