7. 测试篇¶
对应章节:第11章(测试)。
本篇掌握用测试保证代码正确性:单元测试与集成测试。
项目 14:字符串工具库¶
目标:写纯函数库 + 单元测试 + 集成测试。
创建项目¶
说明:这是库项目(--lib),没有 main 函数,代码放在
src/lib.rs。集成测试放在tests/目录(无需手动创建,cargo test 会自动发现tests/*.rs)。
功能要求¶
word_count、reverse、is_palindrome、longest_word- 单元测试(
#[cfg(test)]) - 集成测试(
tests/目录)
知识点¶
| 知识点 | 说明 |
|---|---|
| 纯函数库 | lib.rs 无 main |
#[cfg(test)] |
只在测试编译 |
#[test] |
测试函数 |
assert_eq! / assert! |
断言宏 |
tests/ 目录 |
集成测试 |
use super::* |
测试访问父模块 |
代码¶
src/lib.rs:
// 纯函数:输入 &str,输出计算结果,不依赖外部状态
pub fn word_count(s: &str) -> usize {
s.split_whitespace().count()
}
pub fn reverse(s: &str) -> String {
// chars() 字符迭代,rev() 反转,collect 成 String
s.chars().rev().collect()
}
pub fn is_palindrome(s: &str) -> bool {
// 过滤出字母数字,统一小写,得到"干净"的比较基准
let cleaned: String = s.chars()
.filter(|c| c.is_alphanumeric())
.map(|c| c.to_ascii_lowercase())
.collect();
cleaned == reverse(&cleaned) // 正反相同即回文
}
pub fn longest_word(s: &str) -> &str {
s.split_whitespace()
.max_by_key(|w| w.len()) // 按长度取最大
.unwrap_or("") // 空输入返回空串
}
// ============ 单元测试 ============
// #[cfg(test)]:只在 cargo test 时编译,生产构建剔除
#[cfg(test)]
mod tests {
use super::*; // 引入父模块(lib 根)的所有函数
#[test]
fn word_count_works() {
assert_eq!(word_count("hello world rust"), 3);
}
#[test]
fn reverse_works() {
assert_eq!(reverse("rust"), "tsur");
}
#[test]
fn palindrome_true() {
assert!(is_palindrome("A man, a plan, a canal: Panama"));
}
#[test]
fn palindrome_false() {
assert!(!is_palindrome("hello"));
}
#[test]
fn longest_word_works() {
assert_eq!(longest_word("the quick brown fox"), "quick");
}
// 边界情况也要测:空字符串
#[test]
fn longest_word_empty() {
assert_eq!(longest_word(""), "");
}
}
tests/str_tools.rs(集成测试,从外部视角测公开 API):
// 集成测试文件是独立的 crate,只能访问 pub API
use str_tools::{is_palindrome, reverse, word_count};
#[test]
fn integration_reverse() {
assert_eq!(reverse("abc"), "cba");
}
#[test]
fn integration_word_count() {
assert_eq!(word_count("a b c d"), 4);
}
#[test]
fn integration_palindrome() {
assert!(is_palindrome("racecar"));
}
运行¶
cargo test # 全部测试(单元 + 集成)
cargo test -- --nocapture # 显示 println 输出
cargo test word_count # 只跑名字含 word_count 的测试
运行后会看到 test result: ok. 9 passed 之类的汇总,包含单元测试(src 内)和集成测试(tests/)。
注意事项¶
- 单元测试写在
#[cfg(test)] mod tests:生产代码编译时这个模块被剔除,不增加二进制体积。 use super::*;:测试模块要能访问库内部函数,必须引入父模块。- 集成测试放
tests/:每个文件是独立的 crate,只能测pubAPI——这正是"外部使用者视角"。 chars().rev()反转字符串:&str不能直接reverse(),需转字符迭代器反转后collect::<String>()。
进阶:更多纯函数练习¶
实战中的同名项目 strtools 换了一批函数继续练测试, word_count 在这里换了语义——从"数单词个数"变成"统计每个单词出现次数":
src/lib.rs:
use std::collections::HashMap;
// 首字母大写:"hello" → "Hello"
pub fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => first.to_uppercase().to_string() + chars.as_str(),
None => String::new(), // 空串边界
}
}
pub fn is_palindrome(s: &str) -> bool {
let clean: String = s
.chars()
.filter(|c| c.is_alphanumeric())
.map(|c| c.to_lowercase().next().unwrap()) // to_lowercase 返回迭代器,取第一个字符
.collect();
clean == clean.chars().rev().collect::<String>()
}
// 与原版同名但语义不同:返回 词 → 出现次数 的词频表
pub fn word_count(s: &str) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for word in s.split_whitespace() {
*counts.entry(word.to_string()).or_insert(0) += 1;
}
counts
}
// snake_case → camelCase:"hello_world_foo" → "helloWorldFoo"
pub fn camel_case(s: &str) -> String {
s.split('_')
.enumerate()
.map(|(i, part)| {
if i == 0 {
part.to_string() // 首段保持小写
} else {
let mut chars = part.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capitalize_first_letter() {
assert_eq!(capitalize("hello"), "Hello");
}
#[test]
fn capitalize_handles_empty() {
assert_eq!(capitalize(""), "");
}
#[test]
fn is_palindrome_basic() {
assert!(is_palindrome("racecar"));
assert!(is_palindrome("A man a plan a canal Panama"));
assert!(!is_palindrome("hello"));
}
}
要点¶
- 同名函数,两种 API:原版
word_count(&str) -> usize数总数;进阶版word_count(&str) -> HashMap<String, usize>给词频。函数名传达意图的能力有限,返回类型才是契约——设计 API 时值得琢磨。 chars.next()+chars.as_str()组合:取走第一个字符后,as_str()把迭代器剩余部分变回&str——拆头留尾的惯用法(capitalize/camel_case 两处使用)。to_lowercase()是迭代器:字符的大小写转换可能产生多个字符(如 İ),所以.next().unwrap()取结果;原版的to_ascii_lowercase()直接返回 char,只对 ASCII 安全。entry().or_insert(0)词频套路:不存在则插入 0,再+= 1——HashMap 计数的标准三连。enumerate()区分首元素:camel_case 里第一段不大写,靠索引判断。