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

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

TDD 通常包含以下步骤:

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

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

2. 添加一个失败的测试

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

rust 复制代码
#[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 函数的占位实现:

rust 复制代码
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. 返回该向量。

完整示例:

rust 复制代码
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 测试应当通过,说明最小逻辑已经满足需求。

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

rust 复制代码
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 中输出每条匹配结果。

验证效果

假设命令:

bash 复制代码
$ 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 之路上收获满满!

相关推荐
IT猿手1 分钟前
基于雪雁算法(Snow Geese Algorithm,SGA)的多个无人机协同路径规划(可以自定义无人机数量及起始点),MATLAB代码
开发语言·人工智能·算法·机器学习·matlab·无人机
Matlab仿真实验室21 分钟前
基于Matlab实现机械臂阻抗控制仿真
开发语言·数学建模·matlab·机械臂阻抗控制仿真
EnigmaCoder33 分钟前
C 语言进【进阶篇】之动态内存管理:从底层机制到实战优化
c语言·开发语言
2301_8047744938 分钟前
算法题练习
java·开发语言·算法
珹洺1 小时前
计算机网络:(一)详细讲解互联网概述与组成 (附带图谱更好对比理解)
服务器·开发语言·网络·数据库·后端·计算机网络·php
繁缕怀夕2 小时前
【QT笔记---QText】
开发语言·笔记·qt
byxdaz2 小时前
Qt5中视口(ViewPort)与窗口(Window)
开发语言·qt
jyan_敬言2 小时前
【C++】入门基础(二)引用、const引用、内联函数inline、nullptr
c语言·开发语言·数据结构·c++·青少年编程·编辑器
UpUpUp……2 小时前
模拟String基本函数/深浅拷贝/柔性数组
开发语言·c++·算法
奥顺互联V2 小时前
如何处理PHP中的编码问题
android·开发语言·php