Rust 采用測(cè)試驅(qū)動(dòng)開(kāi)發(fā)完善庫(kù)的功能

2023-03-22 15:12 更新
ch12-04-testing-the-librarys-functionality.md
commit 04170d1feee2a47525b39f1edce77ba615ca9cdf

現(xiàn)在我們將邏輯提取到了 src/lib.rs 并將所有的參數(shù)解析和錯(cuò)誤處理留在了 src/main.rs 中,為代碼的核心功能編寫(xiě)測(cè)試將更加容易。我們可以直接使用多種參數(shù)調(diào)用函數(shù)并檢查返回值而無(wú)需從命令行運(yùn)行二進(jìn)制文件了。

在這一部分,我們將遵循測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(Test Driven Development, TDD)的模式來(lái)逐步增加 minigrep 的搜索邏輯。這是一個(gè)軟件開(kāi)發(fā)技術(shù),它遵循如下步驟:

  1. 編寫(xiě)一個(gè)失敗的測(cè)試,并運(yùn)行它以確保它失敗的原因是你所期望的。
  2. 編寫(xiě)或修改足夠的代碼來(lái)使新的測(cè)試通過(guò)。
  3. 重構(gòu)剛剛增加或修改的代碼,并確保測(cè)試仍然能通過(guò)。
  4. 從步驟 1 開(kāi)始重復(fù)!

這只是眾多編寫(xiě)軟件的方法之一,不過(guò) TDD 有助于驅(qū)動(dòng)代碼的設(shè)計(jì)。在編寫(xiě)能使測(cè)試通過(guò)的代碼之前編寫(xiě)測(cè)試有助于在開(kāi)發(fā)過(guò)程中保持高測(cè)試覆蓋率。

我們將測(cè)試驅(qū)動(dòng)實(shí)現(xiàn)實(shí)際在文件內(nèi)容中搜索查詢字符串并返回匹配的行示例的功能。我們將在一個(gè)叫做 search 的函數(shù)中增加這些功能。

編寫(xiě)失敗測(cè)試

去掉 src/lib.rs 和 src/main.rs 中用于檢查程序行為的 println! 語(yǔ)句,因?yàn)椴辉僬嬲枰麄兞?。接著我們?huì)像 第十一章 那樣增加一個(gè) test 模塊和一個(gè)測(cè)試函數(shù)。測(cè)試函數(shù)指定了 search 函數(shù)期望擁有的行為:它會(huì)獲取一個(gè)需要查詢的字符串和用來(lái)查詢的文本,并只會(huì)返回包含請(qǐng)求的文本行。示例 12-15 展示了這個(gè)測(cè)試,它還不能編譯:

文件名: 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)建一個(gè)我們期望的 search 函數(shù)的失敗測(cè)試

這里選擇使用 "duct" 作為這個(gè)測(cè)試中需要搜索的字符串。用來(lái)搜索的文本有三行,其中只有一行包含 "duct"。(注意雙引號(hào)之后的反斜杠,這告訴 Rust 不要在字符串字面值內(nèi)容的開(kāi)頭加入換行符)我們斷言 search 函數(shù)的返回值只包含期望的那一行。

我們還不能運(yùn)行這個(gè)測(cè)試并看到它失敗,因?yàn)樗踔炼歼€不能編譯:search 函數(shù)還不存在呢!我們將增加足夠的代碼來(lái)使其能夠編譯:一個(gè)總是會(huì)返回空 vector 的 search 函數(shù)定義,如示例 12-16 所示。然后這個(gè)測(cè)試應(yīng)該能夠編譯并因?yàn)榭?vector 并不匹配一個(gè)包含一行 "safe, fast, productive." 的 vector 而失敗。

文件名: src/lib.rs

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    vec![]
}

示例 12-16:剛好足夠使測(cè)試通過(guò)編譯的 search 函數(shù)定義

注意需要在 search 的簽名中定義一個(gè)顯式生命周期 'a 并用于 contents 參數(shù)和返回值?;貞浺幌?nbsp;第十章 中講到生命周期參數(shù)指定哪個(gè)參數(shù)的生命周期與返回值的生命周期相關(guān)聯(lián)。在這個(gè)例子中,我們表明返回的 vector 中應(yīng)該包含引用參數(shù) contents(而不是參數(shù)query) slice 的字符串 slice。

換句話說(shuō),我們告訴 Rust 函數(shù) search 返回的數(shù)據(jù)將與 search 函數(shù)中的參數(shù) contents 的數(shù)據(jù)存在的一樣久。這是非常重要的!為了使這個(gè)引用有效那么  slice 引用的數(shù)據(jù)也需要保持有效;如果編譯器認(rèn)為我們是在創(chuàng)建 query 而不是 contents 的字符串 slice,那么安全檢查將是不正確的。

如果嘗試不用生命周期編譯的話,我們將得到如下錯(cuò)誤:

$ 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 不可能知道我們需要的是哪一個(gè)參數(shù),所以需要告訴它。因?yàn)閰?shù) contents 包含了所有的文本而且我們希望返回匹配的那部分文本,所以我們知道 contents 是應(yīng)該要使用生命周期語(yǔ)法來(lái)與返回值相關(guān)聯(lián)的參數(shù)。

其他語(yǔ)言中并不需要你在函數(shù)簽名中將參數(shù)與返回值相關(guān)聯(lián)。所以這么做可能仍然感覺(jué)有些陌生,隨著時(shí)間的推移這將會(huì)變得越來(lái)越容易。你可能想要將這個(gè)例子與第十章中 “生命周期與引用有效性” 部分做對(duì)比。

現(xiàn)在運(yùn)行測(cè)試:

$ 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'

好的,測(cè)試失敗了,這正是我們所期望的。修改代碼來(lái)讓測(cè)試通過(guò)吧!

編寫(xiě)使測(cè)試通過(guò)的代碼

目前測(cè)試之所以會(huì)失敗是因?yàn)槲覀兛偸欠祷匾粋€(gè)空的 vector。為了修復(fù)并實(shí)現(xiàn) search,我們的程序需要遵循如下步驟:

  • 遍歷內(nèi)容的每一行文本。
  • 查看這一行是否包含要搜索的字符串。
  • 如果有,將這一行加入列表返回值中。
  • 如果沒(méi)有,什么也不做。
  • 返回匹配到的結(jié)果列表

讓我們一步一步的來(lái),從遍歷每行開(kāi)始。

使用 lines 方法遍歷每一行

Rust 有一個(gè)有助于一行一行遍歷字符串的方法,出于方便它被命名為 lines,它如示例 12-17 這樣工作。注意這還不能編譯:

文件名: src/lib.rs

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        // 對(duì)文本行進(jìn)行操作
    }
}

示例 12-17:遍歷 contents 的每一行

lines 方法返回一個(gè)迭代器。第十三章 會(huì)深入了解迭代器,不過(guò)我們已經(jīng)在 示例 3-5 中見(jiàn)過(guò)使用迭代器的方法了,在那里使用了一個(gè) for 循環(huán)和迭代器在一個(gè)集合的每一項(xiàng)上運(yùn)行了一些代碼。

用查詢字符串搜索每一行

接下來(lái)將會(huì)增加檢查當(dāng)前行是否包含查詢字符串的功能。幸運(yùn)的是,字符串類型為此也有一個(gè)叫做 contains 的實(shí)用方法!如示例 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) {
            // 對(duì)文本行進(jìn)行操作
        }
    }
}

示例 12-18:增加檢查文本行是否包含 query 中字符串的功能

存儲(chǔ)匹配的行

我們還需要一個(gè)方法來(lái)存儲(chǔ)包含查詢字符串的行。為此可以在 for 循環(huán)之前創(chuàng)建一個(gè)可變的 vector 并調(diào)用 push 方法在 vector 中存放一個(gè) line。在 for 循環(huán)之后,返回這個(gè) 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:儲(chǔ)存匹配的行以便可以返回他們

現(xiàn)在 search 函數(shù)應(yīng)該返回只包含 query 的那些行,而測(cè)試應(yīng)該會(huì)通過(guò)。讓我們運(yùn)行測(cè)試:

$ 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

測(cè)試通過(guò)了,它可以工作了!

現(xiàn)在正是可以考慮重構(gòu)的時(shí)機(jī),在保證測(cè)試通過(guò),保持功能不變的前提下重構(gòu) search 函數(shù)。search 函數(shù)中的代碼并不壞,不過(guò)并沒(méi)有利用迭代器的一些實(shí)用功能。第十三章將回到這個(gè)例子并深入探索迭代器并看看如何改進(jìn)代碼。

在 run 函數(shù)中使用 search 函數(shù)

現(xiàn)在 search 函數(shù)是可以工作并測(cè)試通過(guò)了的,我們需要實(shí)際在 run 函數(shù)中調(diào)用 search。需要將 config.query 值和 run 從文件中讀取的 contents 傳遞給 search 函數(shù)。接著 run 會(huì)打印出 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 返回的每一行并打印出來(lái)。

現(xiàn)在整個(gè)程序應(yīng)該可以工作了!讓我們?cè)囈辉嚕紫仁褂靡粋€(gè)只會(huì)在艾米莉·狄金森的詩(shī)中返回一行的單詞 “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)在試試一個(gè)會(huì)匹配多行的單詞,比如 “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!

最后,讓我們確保搜索一個(gè)在詩(shī)中哪里都沒(méi)有的單詞時(shí)不會(huì)得到任何行,比如 "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)建了一個(gè)屬于自己的迷你版經(jīng)典工具,并學(xué)習(xí)了很多如何組織程序的知識(shí)。我們還學(xué)習(xí)了一些文件輸入輸出、生命周期、測(cè)試和命令行解析的內(nèi)容。

為了使這個(gè)項(xiàng)目更豐滿,我們將簡(jiǎn)要的展示如何處理環(huán)境變量和打印到標(biāo)準(zhǔn)錯(cuò)誤,這兩者在編寫(xiě)命令行程序時(shí)都很有用。


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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)