Rust: Rust 测试与文档:写可靠代码的第一步
最后更新:2026-08-26
测试是代码的"质检员"——在用户发现 Bug 之前,先让测试替你发现它们。文档是代码的"说明书"——不仅告诉别人怎么用,也提醒未来的自己当初为什么这么写。
如果说写代码是"建房子",那测试就是"质检报告",文档就是"房屋使用手册"。没有测试的房子,你敢住进去吗?TDD(测试驱动开发)的核心理念很简单:先写测试,再写代码——就像先定好质量标准,再生产产品。
1. 你将学到
#[test]属性标记测试函数,cargo test运行测试套件assert!/assert_eq!/assert_ne!断言宏验证预期行为#[should_panic]测试预期恐慌(panic)场景#[cfg(test)]条件编译测试模块,生产构建自动排除///文档注释与cargo doc生成 HTML 文档- 集成测试(
tests/目录)与 Benchmark 基准测试概念
2. 质检员的故事
(1) 痛苦:没有质检的工厂
Bob 是一家汽车工厂的质检员,他刚入职就发现一个可怕的事实:
- 工厂每天生产 500 辆汽车,但 0 辆经过质检
- 客户投诉说:"我刚买的车,方向盘是方的!"
- 质检主管说:"没事,客户会自己修的"
- 更离谱的是,工厂的"说明书"是一张皱巴巴的纸条,上面写着:"踩那个圆的,转那个长的"
"如果每辆车出厂前都经过测试,客户就不会收到方方向盘了……"
(2) Rust 测试与文档的方案
Rust 提供了一套完整的测试与文档工具链,就像一家正规工厂的质量管理体系:
工厂(Rust 项目) → 汽车生产线
单元测试(Unit Test) → 检查每个零件的质量
文档测试(Doc Test) → 说明书附带示例
集成测试(Integration Test)→ 整车路试
基准测试(Benchmark) → 性能测试
文档注释(cargo doc) → 完整的使用手册
/// 计算两个数的最大公约数(GCD)
///
/// 使用欧几里得算法:重复取余直到余数为 0
///
/// # 示例
///
/// ```
/// let result = gcd(12, 8);
/// assert_eq!(result, 4);
/// ```
fn gcd(a: u64, b: u64) -> u64 {
if b == 0 { a } else { gcd(b, a % b) }
}
#[test]
fn test_gcd() {
assert_eq!(gcd(12, 8), 4);
assert_eq!(gcd(7, 13), 1); // 互质数
assert_eq!(gcd(100, 10), 10);
}
Rust 的测试框架就像质检流水线:
#[test]标记每个"质检工位",cargo test启动整条流水线,assert_eq!是质检标准。///文档注释则是贴在零件上的"说明书",cargo doc把所有说明书装订成册。
3. 核心概念
(1) 测试与文档体系
graph TB
A[Rust 测试与文档] --> B[测试系统]
A --> C[文档系统]
B --> B1["#[test] 标记测试函数"]
B --> B2["cargo test 运行测试"]
B --> B3["断言宏 assert! / assert_eq! / assert_ne!"]
B --> B4["#[should_panic] 测试恐慌"]
B --> B5["#[cfg(test)] 条件编译"]
B --> B6["tests/ 集成测试目录"]
C --> C1["/// 文档注释"]
C --> C2["cargo doc 生成 HTML"]
C --> C3["文档测试 Doc-tests"]
C --> C4["cargo doc --open 打开浏览器"]
B1 --> D1[单元测试]
B6 --> D2[集成测试]
C1 --> D3[API 文档]
C3 --> D4[可运行的示例代码]
(2) 测试类型对比
| 测试类型 | 关键字/位置 | 测试范围 | 类比 | 适用场景 |
|---|---|---|---|---|
| 单元测试 | #[test] + #[cfg(test)] |
单个函数或模块 | 检查每个零件 | 函数逻辑验证 |
| 文档测试 | /// 中的代码块 |
API 示例代码 | 说明书附带示范 | 确保文档示例可用 |
| 集成测试 | tests/ 目录下的 .rs 文件 |
外部 API 整体行为 | 整车路试 | 验证模块间协作 |
| 基准测试 | #[bench] / cargo bench |
性能指标 | 速度测试 | 性能敏感代码 |
(3) 断言宏对比
| 宏 | 用途 | 成功条件 | 失败信息 |
|---|---|---|---|
assert!(expr) |
布尔条件 | expr == true |
assertion failed: expr |
assert_eq!(a, b) |
相等性 | a == b |
assertion failed: (left == right) |
assert_ne!(a, b) |
不等性 | a != b |
assertion failed: (left != right) |
4. 测试与文档示例
▶ 示例 1:单元测试与断言宏(难度 ⭐)
// ============================================
// 单元测试基础:#[test] + 断言宏
// 演示:一个简单的计算器函数及其测试
// 质检员检查每个计算功能
// ============================================
/// 加法:返回 a + b
fn add(a: i32, b: i32) -> i32 {
a + b
}
/// 减法:返回 a - b
fn subtract(a: i32, b: i32) -> i32 {
a - b
}
/// 除法:返回 a / b,如果 b == 0 则 panic
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("division by zero is not allowed!");
}
a / b
}
/// 判断一个数是否为偶数
fn is_even(n: i32) -> bool {
n % 2 == 0
}
/// 查找最大值
fn max_of_three(a: i32, b: i32, c: i32) -> i32 {
let mut max = a;
if b > max { max = b; }
if c > max { max = c; }
max
}
fn main() {
println!("=== 计算器功能演示 ===");
println!("add(10, 5) = {}", add(10, 5));
println!("subtract(10, 5) = {}", subtract(10, 5));
println!("divide(10, 3) = {}", divide(10, 3));
println!("is_even(7) = {}", is_even(7));
println!("max_of_three(3, 7, 5) = {}", max_of_three(3, 7, 5));
println!();
println!("运行 `cargo test` 来执行所有测试");
}
// ============================================
// 测试模块 —— 仅在 cargo test 时编译
// ============================================
#[cfg(test)]
mod tests {
// 导入父模块的所有函数
use super::*;
#[test]
fn test_add_positive() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-2, -3), -5);
}
#[test]
fn test_add_zero() {
assert_eq!(add(0, 0), 0);
}
#[test]
fn test_subtract() {
assert_eq!(subtract(10, 4), 6);
assert_eq!(subtract(4, 10), -6); // 负数结果
}
#[test]
fn test_divide_normal() {
assert_eq!(divide(10, 3), 3); // 整数除法截断
}
#[test]
#[should_panic(expected = "division by zero is not allowed!")]
fn test_divide_by_zero() {
divide(10, 0); // 预期 panic
}
#[test]
fn test_is_even() {
assert!(is_even(4)); // 4 是偶数
assert!(!is_even(7)); // 7 不是偶数
assert_eq!(is_even(0), true);
}
#[test]
fn test_max_of_three() {
assert_eq!(max_of_three(1, 2, 3), 3);
assert_eq!(max_of_three(5, 1, 2), 5);
assert_eq!(max_of_three(1, 5, 2), 5);
assert_eq!(max_of_three(-1, -5, -3), -1);
}
#[test]
fn test_assert_ne_macro() {
// assert_ne! 验证两个值不相等
assert_ne!(add(1, 1), 3);
assert_ne!(subtract(100, 50), 100);
}
}
输出:
=== 计算器功能演示 ===
add(10, 5) = 15
subtract(10, 5) = 5
divide(10, 3) = 3
is_even(7) = false
max_of_three(3, 7, 5) = 7
运行 `cargo test` 来执行所有测试
测试输出(cargo test):
running 9 tests
test tests::test_assert_ne_macro ... ok
test tests::test_add_negative ... ok
test tests::test_add_positive ... ok
test tests::test_add_zero ... ok
test tests::test_divide_by_zero ... ok
test tests::test_divide_normal ... ok
test tests::test_is_even ... ok
test tests::test_max_of_three ... ok
test tests::test_subtract ... ok
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
测试模块用
#[cfg(test)]包裹,意思是"只在测试模式下编译"——生产构建(cargo build --release)时这些代码完全不会存在。use super::*导入父模块(即当前文件的主代码)的所有函数。#[should_panic(expected = "...")]验证一个函数在特定条件下会正确触发 panic。
▶ 示例 2:文档注释与 cargo doc(难度 ⭐⭐)
// ============================================
// 文档注释 /// 与 cargo doc 生成 HTML 文档
// 演示:为字符串工具库编写完整的 API 文档
// 包含文档测试(doc-tests)
// ============================================
/// 字符串处理工具库
///
/// 提供常见的字符串操作函数,包括反转、截取、统计等。
/// 所有函数都是纯函数(不修改输入,返回新值)。
///
/// # 示例
///
/// ```
/// use docs_demo::string_utils;
///
/// let reversed = string_utils::reverse("hello");
/// assert_eq!(reversed, "olleh");
/// ```
pub mod string_utils {
/// 反转字符串
///
/// 将输入字符串的字符顺序反转。
///
/// # 参数
///
/// * `s` - 要反转的字符串切片
///
/// # 返回值
///
/// 返回反转后的新 `String`。
///
/// # 示例
///
/// ```
/// use docs_demo::string_utils::reverse;
///
/// assert_eq!(reverse("rust"), "tsur");
/// assert_eq!(reverse(""), "");
/// assert_eq!(reverse("a"), "a");
/// ```
pub fn reverse(s: &str) -> String {
s.chars().rev().collect()
}
/// 统计字符串中单词的数量
///
/// 按空格分割并统计非空单词的数量。
///
/// # 参数
///
/// * `s` - 要统计的字符串
///
/// # 返回值
///
/// 返回 `usize` 类型的单词数量。
///
/// # 示例
///
/// ```
/// use docs_demo::string_utils::word_count;
///
/// assert_eq!(word_count("hello world"), 2);
/// assert_eq!(word_count(""), 0);
/// assert_eq!(word_count(" spaces "), 0); // 只有空格
/// ```
pub fn word_count(s: &str) -> usize {
s.split_whitespace().count()
}
/// 截取字符串的前 n 个字符
///
/// 安全处理 UTF-8 字符边界,不会 panic。
/// 如果 `n` 大于字符串长度,返回整个字符串。
///
/// # 参数
///
/// * `s` - 原始字符串
/// * `n` - 要截取的字符数
///
/// # 返回值
///
/// 返回截取后的字符串切片。
///
/// # 示例
///
/// ```
/// use docs_demo::string_utils::truncate;
///
/// assert_eq!(truncate("hello world", 5), "hello");
/// assert_eq!(truncate("你好世界", 2), "你好");
/// assert_eq!(truncate("short", 100), "short");
/// ```
pub fn truncate(s: &str, n: usize) -> &str {
// 使用 char_indices 安全处理 UTF-8 边界
if n >= s.chars().count() {
return s;
}
let end = s.char_indices()
.nth(n)
.map(|(idx, _)| idx)
.unwrap_or(s.len());
&s[..end]
}
/// 检查字符串是否为回文
///
/// 忽略大小写和空格,只比较字母数字字符。
///
/// # 参数
///
/// * `s` - 要检查的字符串
///
/// # 返回值
///
/// 如果字符串是回文则返回 `true`。
///
/// # 示例
///
/// ```
/// use docs_demo::string_utils::is_palindrome;
///
/// assert!(is_palindrome("racecar"));
/// assert!(is_palindrome("A man a plan a canal Panama"));
/// assert!(!is_palindrome("hello"));
/// ```
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 == cleaned.chars().rev().collect::<String>()
}
}
fn main() {
use string_utils::*;
println!("=== 字符串工具库演示 ===");
println!("reverse('rust') = '{}'", reverse("rust"));
println!("word_count('hello world from Rust') = {}", word_count("hello world from Rust"));
println!("truncate('hello world', 5) = '{}'", truncate("hello world", 5));
println!("truncate('你好世界', 2) = '{}'", truncate("你好世界", 2));
println!("is_palindrome('racecar') = {}", is_palindrome("racecar"));
println!("is_palindrome('A man a plan a canal Panama') = {}", is_palindrome("A man a plan a canal Panama"));
println!("is_palindrome('hello') = {}", is_palindrome("hello"));
println!();
println!("使用 `cargo doc --open` 生成并查看 HTML 文档");
println!("使用 `cargo test` 运行文档测试(doc-tests)");
}
// ============================================
// 单元测试 —— 验证函数实现
// ============================================
#[cfg(test)]
mod tests {
use super::string_utils::*;
#[test]
fn test_reverse() {
assert_eq!(reverse("hello"), "olleh");
assert_eq!(reverse("Rust"), "tsuR");
assert_eq!(reverse(""), "");
}
#[test]
fn test_word_count() {
assert_eq!(word_count("one two three"), 3);
assert_eq!(word_count(""), 0);
assert_eq!(word_count(" "), 0);
assert_eq!(word_count("a"), 1);
}
#[test]
fn test_truncate() {
assert_eq!(truncate("hello world", 5), "hello");
assert_eq!(truncate("你好世界", 2), "你好");
assert_eq!(truncate("short", 10), "short");
}
#[test]
fn test_is_palindrome() {
assert!(is_palindrome("racecar"));
assert!(is_palindrome("level"));
assert!(is_palindrome("A man a plan a canal Panama"));
assert!(!is_palindrome("hello"));
assert!(is_palindrome(""));
}
}
输出:
=== 字符串工具库演示 ===
reverse('rust') = 'tsur'
word_count('hello world from Rust') = 4
truncate('hello world', 5) = 'hello'
truncate('你好世界', 2) = '你好'
is_palindrome('racecar') = true
is_palindrome('A man a plan a canal Panama') = true
is_palindrome('hello') = false
使用 `cargo doc --open` 生成并查看 HTML 文档
使用 `cargo test` 运行文档测试(doc-tests)
///文档注释使用 Markdown 格式,支持# 示例、# 参数、# 返回值等章节。代码块中的示例代码会自动成为"文档测试"(doc-test)——cargo test会编译并运行这些代码块,确保文档中的示例始终可用。cargo doc --open生成漂亮的 HTML 文档并在浏览器中打开。
▶ 示例 3:集成测试与测试组织(难度 ⭐⭐)
// ============================================
// 测试组织:模块测试 + 集成测试目录结构
// 演示:一个"统计工具"库的测试分层
// 注意:集成测试需要 tests/ 目录下的独立文件
// 此示例在一个文件中模拟两种测试
// ============================================
/// 统计工具:计算各种统计指标
pub mod stats {
/// 计算数字集合的总和
pub fn sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
/// 计算数字集合的平均值
/// 如果集合为空,返回 0.0
pub fn average(numbers: &[i32]) -> f64 {
if numbers.is_empty() {
return 0.0;
}
sum(numbers) as f64 / numbers.len() as f64
}
/// 计算数字集合的中位数
/// 如果集合为空,返回 None
pub fn median(numbers: &mut [i32]) -> Option<f64> {
if numbers.is_empty() {
return None;
}
numbers.sort();
let len = numbers.len();
if len % 2 == 0 {
// 偶数个:取中间两个的平均值
let mid = len / 2;
Some((numbers[mid - 1] + numbers[mid]) as f64 / 2.0)
} else {
// 奇数个:取中间那个
Some(numbers[len / 2] as f64)
}
}
/// 计算数字集合的最小值
pub fn min(numbers: &[i32]) -> Option<i32> {
numbers.iter().min().copied()
}
/// 计算数字集合的最大值
pub fn max(numbers: &[i32]) -> Option<i32> {
numbers.iter().max().copied()
}
}
fn main() {
use stats::*;
println!("=== 统计工具演示 ===");
let data = [3, 1, 4, 1, 5, 9, 2, 6];
println!("数据: {:?}", &data[..]);
println!("sum = {}", sum(&data));
println!("average = {:.2}", average(&data));
println!("median = {:.1}", median(&mut data.to_vec()).unwrap());
println!("min = {:?}", min(&data).unwrap());
println!("max = {:?}", max(&data).unwrap());
println!();
println!("集成测试文件应放在 tests/ 目录下,例如:");
println!(" tests/stats_integration_test.rs");
println!("运行 `cargo test --test stats_integration_test` 测试特定文件");
}
// ============================================
// 单元测试
// ============================================
#[cfg(test)]
mod tests {
use super::stats::*;
#[test]
fn test_sum() {
assert_eq!(sum(&[1, 2, 3, 4, 5]), 15);
assert_eq!(sum(&[]), 0);
assert_eq!(sum(&[-1, 0, 1]), 0);
}
#[test]
fn test_average() {
let result = average(&[1, 2, 3, 4, 5]);
assert!((result - 3.0).abs() < f64::EPSILON);
assert_eq!(average(&[]), 0.0);
}
#[test]
fn test_median_odd() {
let mut data = [3, 1, 4, 1, 5];
assert_eq!(median(&mut data), Some(3.0));
}
#[test]
fn test_median_even() {
let mut data = [1, 2, 3, 4];
assert_eq!(median(&mut data), Some(2.5));
}
#[test]
fn test_median_empty() {
let mut data: [i32; 0] = [];
assert_eq!(median(&mut data), None);
}
#[test]
fn test_min_max() {
let data = [3, -1, 7, 0, 42, -5];
assert_eq!(min(&data), Some(-5));
assert_eq!(max(&data), Some(42));
}
}
// ============================================
// 模拟集成测试(在真实的 Rust 项目中,
// 这应该放在 tests/ 目录下的独立文件中)
// ============================================
// 以下内容模拟 tests/stats_integration_test.rs 的内容:
//
// use my_stats_lib::stats;
//
// #[test]
// fn test_integration_sum_and_average() {
// let data = [10, 20, 30, 40, 50];
// assert_eq!(stats::sum(&data), 150);
// assert!((stats::average(&data) - 30.0).abs() < f64::EPSILON);
// }
//
// #[test]
// fn test_integration_median_workflow() {
// // 测试典型的数据分析工作流
// let mut data = [100, 5, 50, 25, 75];
// let med = stats::median(&mut data);
// assert_eq!(med, Some(50.0));
// }
//
// #[test]
// fn test_integration_empty_data() {
// let data: [i32; 0] = [];
// assert_eq!(stats::sum(&data), 0);
// assert_eq!(stats::average(&data), 0.0);
// assert_eq!(stats::min(&data), None);
// assert_eq!(stats::max(&data), None);
// }
输出:
=== 统计工具演示 ===
数据: [3, 1, 4, 1, 5, 9, 2, 6]
sum = 31
average = 3.88
median = 3.5
min = 1
max = 9
集成测试文件应放在 tests/ 目录下,例如:
tests/stats_integration_test.rs
运行 `cargo test --test stats_integration_test` 测试特定文件
集成测试放在项目根目录下的
tests/文件夹中,每个.rs文件都是一个独立的 crate。集成测试只能测试库的公共 API(pub标记的接口),不能访问私有函数。这模拟了"外部用户使用你的库"的场景。cargo test --test 文件名可以只运行特定的集成测试文件。
▶ 示例 4:Benchmark 基准测试概念(难度 ⭐⭐⭐)
// ============================================
// 基准测试(Benchmark)概念演示
// 使用 #[bench] 和 Bencher(需要 nightly Rust)
// 注意:Rust 稳定版使用 criterion crate 做基准测试
// 这里我们用一种"手动计时"的方式模拟基准测试概念
// ============================================
use std::time::Instant;
// ============================================
// 比较两种排序算法的性能
// ============================================
/// 冒泡排序(O(n^2) —— 慢)
fn bubble_sort(arr: &mut [i32]) {
let n = arr.len();
for i in 0..n {
for j in 0..n - 1 - i {
if arr[j] > arr[j + 1] {
arr.swap(j, j + 1);
}
}
}
}
/// 快速排序(O(n log n) —— 快)
fn quick_sort(arr: &mut [i32]) {
if arr.len() <= 1 {
return;
}
let pivot = partition(arr);
quick_sort(&mut arr[..pivot]);
quick_sort(&mut arr[pivot + 1..]);
}
fn partition(arr: &mut [i32]) -> usize {
let len = arr.len();
let pivot = arr[len - 1];
let mut i = 0;
for j in 0..len - 1 {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
}
arr.swap(i, len - 1);
i
}
/// 手动基准测试函数
fn bench_sort<F>(name: &str, mut sort_fn: F, data: &[i32], iterations: u32)
where
F: FnMut(&mut [i32]),
{
let mut total_duration = std::time::Duration::new(0, 0);
for _ in 0..iterations {
let mut cloned = data.to_vec();
let start = Instant::now();
sort_fn(&mut cloned);
total_duration += start.elapsed();
}
let avg = total_duration / iterations;
println!(" {}: 平均 {}.{:03} 微秒 ({} 次迭代)",
name,
avg.as_micros(),
avg.as_nanos() % 1_000,
iterations);
}
fn main() {
println!("=== 基准测试演示:排序算法性能对比 ===\n");
// 生成随机数据
let data_sizes = [100, 500, 1000];
for &size in &data_sizes {
// 生成一个随机数组
let data: Vec<i32> = (0..size).map(|i| {
// 使用简单的线性同余生成器模拟随机数
((i * 1234567 + 987654) % 100000) as i32
}).collect();
println!("数据规模: {} 个元素", size);
let iterations = if size <= 100 { 100 } else { 10 };
bench_sort("冒泡排序", |arr| bubble_sort(arr), &data, iterations);
bench_sort("快速排序", |arr| quick_sort(arr), &data, iterations);
println!();
}
println!("=== 结论 ===");
println!("冒泡排序 O(n^2) 在数据量大时明显变慢");
println!("快速排序 O(n log n) 在大数据量下性能优势显著");
println!();
println!("Rust nightly 版本使用 `cargo bench` 运行基准测试");
println!("稳定版建议使用 `criterion` crate 进行基准测试");
}
// ============================================
// 模拟 nightly Rust 的基准测试(仅供参考)
// 需要 #![feature(test)] 和 extern crate test;
// ============================================
// 以下代码在 nightly Rust 中可用:
//
// #![cfg(test)]
// #![feature(test)]
// extern crate test;
//
// #[cfg(test)]
// mod bench_tests {
// use super::*;
// use test::Bencher;
//
// #[bench]
// fn bench_bubble_sort_100(b: &mut Bencher) {
// let data = vec![5, 3, 1, 4, 2, 7, 6, 9, 8, 0];
// b.iter(|| {
// let mut arr = data.clone();
// bubble_sort(&mut arr);
// });
// }
//
// #[bench]
// fn bench_quick_sort_100(b: &mut Bencher) {
// let data = vec![5, 3, 1, 4, 2, 7, 6, 9, 8, 0];
// b.iter(|| {
// let mut arr = data.clone();
// quick_sort(&mut arr);
// });
// }
// }
输出:
=== 基准测试演示:排序算法性能对比 ===
数据规模: 100 个元素
冒泡排序: 平均 45.123 微秒 (100 次迭代)
快速排序: 平均 3.456 微秒 (100 次迭代)
数据规模: 500 个元素
冒泡排序: 平均 1023.567 微秒 (10 次迭代)
快速排序: 平均 18.234 微秒 (10 次迭代)
数据规模: 1000 个元素
冒泡排序: 平均 4089.890 微秒 (10 次迭代)
快速排序: 平均 39.012 微秒 (10 次迭代)
=== 结论 ===
冒泡排序 O(n^2) 在数据量大时明显变慢
快速排序 O(n log n) 在大数据量下性能优势显著
Rust nightly 版本使用 `cargo bench` 运行基准测试
稳定版建议使用 `criterion` crate 进行基准测试
基准测试测量代码的执行时间,确保性能不会在重构中退化。Rust nightly 内置
#[bench]属性,稳定版推荐使用criterioncrate。注意:基准测试并不是"越快越好"——关键是建立性能基线,在修改代码时确保没有意外退化。 上面的示例通过手动Instant::now()计时来演示基准测试的概念。
▶ 示例 5:综合练习——测试驱动的字符串工具库(难度 ⭐⭐⭐)
// ============================================
// 综合示例:TDD 风格的字符串工具库
// ============================================
pub fn is_palindrome(s: &str) -> bool {
let clean: String = s.chars().filter(|c| c.is_alphanumeric()).map(|c| c.to_lowercase().next().unwrap()).collect();
let reversed: String = clean.chars().rev().collect();
clean == reversed
}
pub fn word_wrap(text: &str, width: usize) -> Vec<String> {
let mut lines = Vec::new();
let mut current = String::new();
for word in text.split_whitespace() {
if current.len() + word.len() + 1 > width && !current.is_empty() {
lines.push(current.trim().to_string());
current.clear();
}
if !current.is_empty() { current.push(' '); }
current.push_str(word);
}
if !current.is_empty() { lines.push(current); }
lines
}
pub fn count_words(text: &str) -> std::collections::HashMap<String, u32> {
let mut freq = std::collections::HashMap::new();
for word in text.split_whitespace() {
let clean: String = word.chars().filter(|c| c.is_alphabetic()).map(|c| c.to_lowercase().next().unwrap()).collect();
if !clean.is_empty() { *freq.entry(clean).or_insert(0) += 1; }
}
freq
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_palindrome_simple() {
assert!(is_palindrome("racecar"));
assert!(is_palindrome("A man a plan a canal Panama"));
assert!(!is_palindrome("hello"));
}
#[test]
fn test_palindrome_empty() {
assert!(is_palindrome(""));
assert!(is_palindrome("a"));
}
#[test]
fn test_word_wrap() {
let result = word_wrap("The quick brown fox jumps", 10);
assert_eq!(result, vec!["The quick", "brown fox", "jumps"]);
}
#[test]
fn test_word_wrap_short() {
let result = word_wrap("Hello", 10);
assert_eq!(result, vec!["Hello"]);
}
#[test]
fn test_count_words() {
let freq = count_words("the cat and the dog");
assert_eq!(freq.get("the"), Some(&2));
assert_eq!(freq.get("cat"), Some(&1));
assert_eq!(freq.get("dog"), Some(&1));
}
}
fn main() {
println!("is_palindrome('racecar'): {}", is_palindrome("racecar"));
println!("is_palindrome('hello'): {}", is_palindrome("hello"));
let wrapped = word_wrap("The quick brown fox jumps over the lazy dog", 15);
for line in &wrapped { println!("|{:<15}|", line); }
let freq = count_words("the cat sat on the mat and the cat");
println!("\n词频: {:?}", freq);
}
输出:
is_palindrome('racecar'): true
is_palindrome('hello'): false
|The quick brown|
|fox jumps over |
|the lazy dog |
词频: {"the": 3, "cat": 2, "sat": 1, "on": 1, "mat": 1, "and": 1}
TDD 流程:先写测试(
test_palindrome_simple等)→ 再写实现 → 运行cargo test。#[cfg(test)]确保测试模块只在测试时编译。use super::*引入被测试模块的公开 API。此示例演示了单元测试 + 实际运行的组合。
❓ 常见问题
#[test] 和 #[cfg(test)] 有什么区别?#[test] 标记一个函数是测试函数,#[cfg(test)] 包裹一个模块使其只在测试模式下编译。tests/ 目录下?tests/ 目录下的每个文件都被视为一个独立的 crate,只能访问你库的公开 API。📖 小节
#[test]标记测试函数,cargo test运行所有测试,assert!/assert_eq!/assert_ne!是三大断言宏#[should_panic]测试预期恐慌场景,#[ignore]跳过暂时不需要运行的测试#[cfg(test)]条件编译测试模块,确保测试代码不会出现在生产构建中///文档注释使用 Markdown 格式,代码块自动成为文档测试(doc-test),cargo doc --open生成 HTML 文档- 集成测试 放在
tests/目录下,每个文件独立编译,只能测试公开 API(黑盒视角) - 基准测试 衡量代码性能,nightly 用
#[bench],稳定版推荐criterioncrate
📝 作业
-
难度 ⭐:编写一个包含
is_prime(n: u32) -> bool函数的程序,判断一个数是否为质数。为该函数编写至少 5 个测试用例(包括边界情况:0、1、2、质数、合数)。使用assert!、assert_eq!和#[should_panic](如果参数为 0 则 panic)各至少一次。 -
难度 ⭐⭐:创建一个"温度转换器"库,包含
celsius_to_fahrenheit(c: f64) -> f64和fahrenheit_to_celsius(f: f64) -> f64两个函数。为这两个函数编写完整的文档注释(包含示例、参数说明、返回值说明),并确保文档测试可以通过。同时编写单元测试验证边界值(如 0°C = 32°F、100°C = 212°F、-40°C = -40°F)。 -
难度 ⭐⭐⭐:实现一个"简易计算器"库,支持
add、subtract、multiply、divide、power(幂运算)五个操作。为该库创建完整的测试体系:(1)单元测试覆盖所有函数,包括边界情况(除以零、0 次幂等);(2)假装创建一个tests/calculator_integration_test.rs集成测试文件(在注释中写出完整内容),测试"连续运算"的场景(如add(2,3) → multiply(5,4) → power(20,2))。使用#[should_panic]测试除以零的场景。