Rust: Rust 测试与文档:写可靠代码的第一步

最后更新:2026-08-26

测试是代码的"质检员"——在用户发现 Bug 之前,先让测试替你发现它们。文档是代码的"说明书"——不仅告诉别人怎么用,也提醒未来的自己当初为什么这么写。

如果说写代码是"建房子",那测试就是"质检报告",文档就是"房屋使用手册"。没有测试的房子,你敢住进去吗?TDD(测试驱动开发)的核心理念很简单:先写测试,再写代码——就像先定好质量标准,再生产产品。


1. 你将学到


2. 质检员的故事

(1) 痛苦:没有质检的工厂

Bob 是一家汽车工厂的质检员,他刚入职就发现一个可怕的事实:

"如果每辆车出厂前都经过测试,客户就不会收到方方向盘了……"

(2) Rust 测试与文档的方案

Rust 提供了一套完整的测试与文档工具链,就像一家正规工厂的质量管理体系:

TEXT 📖 仅展示
工厂(Rust 项目)           → 汽车生产线
  单元测试(Unit Test)      → 检查每个零件的质量
  文档测试(Doc Test)       → 说明书附带示例
  集成测试(Integration Test)→ 整车路试
  基准测试(Benchmark)       → 性能测试
  文档注释(cargo doc)       → 完整的使用手册
RUST
/// 计算两个数的最大公约数(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) 测试与文档体系

100%
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:单元测试与断言宏(难度 ⭐)

RUST
// ============================================
// 单元测试基础:#[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);
    }
}

输出:

TEXT 📖 仅展示
=== 计算器功能演示 ===
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):

TEXT 📖 仅展示
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(难度 ⭐⭐)

RUST
// ============================================
// 文档注释 /// 与 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(""));
    }
}

输出:

TEXT 📖 仅展示
=== 字符串工具库演示 ===
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:集成测试与测试组织(难度 ⭐⭐)

RUST
// ============================================
// 测试组织:模块测试 + 集成测试目录结构
// 演示:一个"统计工具"库的测试分层
// 注意:集成测试需要 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);
// }

输出:

TEXT 📖 仅展示
=== 统计工具演示 ===
数据: [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。集成测试只能测试库的公共 APIpub 标记的接口),不能访问私有函数。这模拟了"外部用户使用你的库"的场景。cargo test --test 文件名 可以只运行特定的集成测试文件。


▶ 示例 4:Benchmark 基准测试概念(难度 ⭐⭐⭐)

RUST
// ============================================
// 基准测试(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);
//         });
//     }
// }

输出:

TEXT 📖 仅展示
=== 基准测试演示:排序算法性能对比 ===

数据规模: 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] 属性,稳定版推荐使用 criterion crate。注意:基准测试并不是"越快越好"——关键是建立性能基线,在修改代码时确保没有意外退化。 上面的示例通过手动 Instant::now() 计时来演示基准测试的概念。


▶ 示例 5:综合练习——测试驱动的字符串工具库(难度 ⭐⭐⭐)

RUST
// ============================================
// 综合示例: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);
}

输出:

TEXT 📖 仅展示
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。此示例演示了单元测试 + 实际运行的组合。


❓ 常见问题

Q #[test]#[cfg(test)] 有什么区别?
A #[test] 标记一个函数是测试函数,#[cfg(test)] 包裹一个模块使其只在测试模式下编译。
Q 文档测试(doc-test)和单元测试有什么区别?
A 文档测试验证文档中的示例代码是否可以正确运行,单元测试验证函数逻辑是否正确。
Q 集成测试为什么要放在 tests/ 目录下?
A 因为 tests/ 目录下的每个文件都被视为一个独立的 crate,只能访问你库的公开 API。

📖 小节


📝 作业

  1. 难度 ⭐:编写一个包含 is_prime(n: u32) -> bool 函数的程序,判断一个数是否为质数。为该函数编写至少 5 个测试用例(包括边界情况:0、1、2、质数、合数)。使用 assert!assert_eq!#[should_panic](如果参数为 0 则 panic)各至少一次。

  2. 难度 ⭐⭐:创建一个"温度转换器"库,包含 celsius_to_fahrenheit(c: f64) -> f64fahrenheit_to_celsius(f: f64) -> f64 两个函数。为这两个函数编写完整的文档注释(包含示例、参数说明、返回值说明),并确保文档测试可以通过。同时编写单元测试验证边界值(如 0°C = 32°F、100°C = 212°F、-40°C = -40°F)。

  3. 难度 ⭐⭐⭐:实现一个"简易计算器"库,支持 addsubtractmultiplydividepower(幂运算)五个操作。为该库创建完整的测试体系:(1)单元测试覆盖所有函数,包括边界情况(除以零、0 次幂等);(2)假装创建一个 tests/calculator_integration_test.rs 集成测试文件(在注释中写出完整内容),测试"连续运算"的场景(如 add(2,3) → multiply(5,4) → power(20,2))。使用 #[should_panic] 测试除以零的场景。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏