13|测试:锁定文本统计的边界行为
目标:把统计契约写成可重复执行的单元测试与公共接口测试,并验证测试能抓住错误。
看到一份正常输出,只能证明那个输入碰巧工作。TextLab 最需要保护的行为是容易误解的边界:空串、汉字、CRLF 和中间空行。测试先说明预期,再让每次修改自动与它比较。
带 #[test] 的函数由 Rust 测试框架执行;测试正常结束即通过,断言失败会让该测试失败。放在模块内部的单元测试可以访问模块细节;tests/ 下的集成测试像外部调用者,只能使用公共 API。测试不是把函数实现复制一遍,而是选择能区分正确与错误行为的输入和结果。
给报告增加可比较能力
以第 12 课检查点为起点。只在 src/stats.rs 的 pub struct Report 正上方加入:
#[derive(Debug, PartialEq, Eq)]
这是属性片段,不是独立程序。derive 为字段支持这些能力的结构体自动生成实现。Debug 支持断言失败时显示值,PartialEq 支持 ==,Eq 表明相等关系满足完整的等价关系要求;本报告全是整数,可以派生这些能力。
在同一文件末尾追加完整测试模块:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_text_has_zero_counts() {
assert_eq!(
analyze(""),
Report { lines: 0, words: 0, bytes: 0, chars: 0 }
);
}
#[test]
fn unicode_units_are_distinct() {
assert_eq!(
analyze("你好 Rust\n"),
Report { lines: 1, words: 2, bytes: 12, chars: 8 }
);
}
#[test]
fn crlf_is_one_line_ending() {
assert_eq!(
analyze("a\r\nb\r\n"),
Report { lines: 2, words: 2, bytes: 6, chars: 6 }
);
}
#[test]
fn interior_blank_line_is_counted() {
let report = analyze("a\n\nb");
assert_eq!(report.lines, 3);
assert_eq!(report.words, 2);
}
}
#[cfg(test)] 让这个模块只在测试配置中编译,super 指父模块,use super::* 让测试使用父模块的条目。测试没有依赖磁盘、网络或执行顺序。
在项目根创建 tests/,然后创建 tests/public_api.rs:
use textlab::analyze;
#[test]
fn summary_is_available_through_the_public_api() {
assert_eq!(
analyze("a b").summary(),
"lines=1 words=2 bytes=3 chars=3"
);
}
运行前先预测:一共应执行多少个带 #[test] 的测试?
cargo test --quiet
预期观察:库测试组 4 个通过,集成测试组 1 个通过,其他目标可能显示 0 个测试。输出顺序和耗时可变,但不应有失败。这里既验证计算,也验证调用者确实能从库根访问函数和摘要方法。
证明断言有作用
- 暂时把
analyze中的chars: text.chars().count()改成chars: text.len()。 - 运行
cargo test --quiet,unicode_units_are_distinct应失败,实际字符数 12 与预期 8 不同。 - 恢复正确实现,再运行全部测试。所有测试应恢复通过。
独立练习:增加一个测试,验证 "a\n" 是 1 行、"\n" 是 1 行、"\n\n" 是 2 行。把测试放进已有单元测试模块,最终总数应为 6。
参考测试
以下函数插入现有 mod tests 内:
#[test]
fn trailing_newline_does_not_create_an_extra_line() {
assert_eq!(analyze("a\n").lines, 1);
assert_eq!(analyze("\n").lines, 1);
assert_eq!(analyze("\n\n").lines, 2);
}
运行 cargo test --quiet。新增测试应通过;把所有行数都简单写成“换行数量加一”会被这些输入中的多个情况识破。后续课程允许保留此测试,因此会有 5 个单元测试和 1 个集成测试。
如果出现无法用 == 比较报告,检查 PartialEq 是否加在结构体上。如果显示 0 个测试,检查文件位置、#[test] 属性以及命令是否在正确的 Cargo 项目中执行。新增断言失败时先手工核对输入的字节和换行,再判断实现是否错误,不要直接把期望值改成当前输出。
检查点:保留第 12 课全部文件,src/stats.rs 新增派生属性与测试模块,新增 tests/public_api.rs。