1. 测试驱动开发(TDD)简介
TDD 通常包含以下步骤:
- 编写一个会失败的测试,并确保它因我们期望的原因而失败。
- 仅编写足够的代码 让这个新测试通过。
- 重构 刚才写的代码,并保证所有测试仍然通过。
- 重复 步骤 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
的行。需要的步骤包括:
- 按行迭代
contents
; - 判断该行是否包含
query
; - 如果包含,推入一个结果向量;
- 返回该向量。
完整示例:
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
测试应当通过,说明最小逻辑已经满足需求。
5. 使用 search
函数
搜索逻辑完成后,我们就能在 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
的实现虽然可用,但:
- 可用迭代器链简化 :在后续我们介绍迭代器时,可以将
for
循环替换为更函数式的写法,比如使用.filter
、.collect
等提高简洁性。 - 区分大小写或添加更多功能 :比如做一个
search_case_insensitive
; - 更多测试场景:可以补充多行、多匹配、不匹配等各式测试,进一步保证搜索逻辑稳健。
TDD 并非唯一可行的方法,但它在很多场景能够驱动你更加清晰地写出高覆盖率的测试,从而持续检验你的设计与需求是否一致。
7. 总结
在本篇中,我们展示了如何利用 测试驱动开发(TDD) 为 minigrep
加入关键搜索功能:
- 先写一个失败测试,确定所需的函数签名与期望行为;
- 实现最小可行逻辑 让测试通过;
- 在实际代码中使用 并继续测试或改进。
TDD 在 Rust 中的实践尤为便利:
- 将核心逻辑提取到
lib.rs
便于直接调用函数测试; cargo test
快速运行与报告;- 随时用单元测试来检验我们的迭代改动。
到此,"minigrep" 工具已经能够读取文本文件、搜索指定关键词并打印结果。后续若要拓展(如环境变量设置、区分大小写等),也可借鉴相同思路,再补充更多测试用例,不断迭代。希望这对你在 Rust CLI 项目中实施 TDD 带来一些灵感与帮助!
祝你在 Rust 的 TDD 之路上收获满满!