用 TDD 构建 Rust 命令行搜索功能:以 minigrep 为例

发布于:2025-02-12 ⋅ 阅读:(11) ⋅ 点赞:(0)

1. 测试驱动开发(TDD)简介

TDD 通常包含以下步骤:

  1. 编写一个会失败的测试,并确保它因我们期望的原因而失败。
  2. 仅编写足够的代码 让这个新测试通过。
  3. 重构 刚才写的代码,并保证所有测试仍然通过。
  4. 重复 步骤 1~3,不断迭代。

这种流程可以帮助我们保持较高的测试覆盖率,同时让需求或 API 在实现之前就被“测试驱动”明确下来。

2. 添加一个失败的测试

src/lib.rs 中,我们先移除调试用的 println!,并添加一个新的测试模块 tests。我们打算写一个函数 search(query, contents),返回所有包含 query 的行。以下是先行编写的测试(暂时不会编译通过):

#[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)
        );
    }
}

测试说明

  • 我们的 query"duct"
  • contents 包含 3 行字符串,其中只有 "safe, fast, productive." 包含 “duct”。
  • 测试断言期望返回一个字符串切片向量,其中只有那一行。

如果此时我们尝试 cargo test,会发现编译都过不去:search 函数根本没有定义。我们要先写一个最简单的函数签名以让它能编译并执行测试(即让测试真正“失败”)。

3. 让测试编译,但先故意失败

src/lib.rs 中新增一个 search 函数的占位实现:

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    Vec::new() // 暂时返回空
}
  • 这里指定了显式生命周期 'a,用于表明返回的切片依赖于 contents 的生命周期(而非 query)。
  • 目前我们只返回一个空向量,让测试会必然失败。

现在再 cargo test 会发现测试失败,且原因是结果为空,不匹配我们期望的那行内容。很好,这正是我们想在 TDD 第一步看到的现象。

4. 编写通过测试的最小实现

既然测试失败,接下来就在 search 函数里实现搜索逻辑,让它只返回包含 query 的行。需要的步骤包括:

  1. 按行迭代 contents
  2. 判断该行是否包含 query
  3. 如果包含,推入一个结果向量;
  4. 返回该向量。

完整示例:

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
}
  • contents.lines() 会逐行迭代文本;
  • line.contains(query) 判断该行是否包含搜索词;
  • 若包含则 push 进结果向量。
  • 最终返回所有匹配行的集合。

再次测试

运行 cargo test,如果一切顺利,one_result 测试应当通过,说明最小逻辑已经满足需求。

5. 使用 search 函数

搜索逻辑完成后,我们就能在 run 函数(lib.rs 里)里调用它。示例:

pub fn run(config: Config) -> Result<(), Box<dyn std::error::Error>> {
    let contents = fs::read_to_string(&config.file_path)?;
    
    // 调用 search
    let results = search(&config.query, &contents);

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

这样就能在 CLI 中输出每条匹配结果。

验证效果

假设命令:

$ cargo run -- body poem.txt

如果 poem.txt 中包含多行带有 “body” 的内容,终端会输出相应的行。搜不到时则不输出。

6. 思考与改进

当前 search 的实现虽然可用,但:

  1. 可用迭代器链简化:在后续我们介绍迭代器时,可以将 for 循环替换为更函数式的写法,比如使用 .filter.collect 等提高简洁性。
  2. 区分大小写或添加更多功能:比如做一个 search_case_insensitive
  3. 更多测试场景:可以补充多行、多匹配、不匹配等各式测试,进一步保证搜索逻辑稳健。

TDD 并非唯一可行的方法,但它在很多场景能够驱动你更加清晰地写出高覆盖率的测试,从而持续检验你的设计与需求是否一致。

7. 总结

在本篇中,我们展示了如何利用 测试驱动开发(TDD)minigrep 加入关键搜索功能:

  1. 先写一个失败测试,确定所需的函数签名与期望行为;
  2. 实现最小可行逻辑 让测试通过;
  3. 在实际代码中使用 并继续测试或改进。

TDD 在 Rust 中的实践尤为便利:

  • 将核心逻辑提取到 lib.rs 便于直接调用函数测试;
  • cargo test 快速运行与报告;
  • 随时用单元测试来检验我们的迭代改动。

到此,“minigrep” 工具已经能够读取文本文件、搜索指定关键词并打印结果。后续若要拓展(如环境变量设置、区分大小写等),也可借鉴相同思路,再补充更多测试用例,不断迭代。希望这对你在 Rust CLI 项目中实施 TDD 带来一些灵感与帮助!

祝你在 Rust 的 TDD 之路上收获满满!