🦀 第17课:测试与文档

实用工具 第17/25课

Rust内置了强大的测试框架和文档生成工具。无需额外依赖,就能编写单元测试、集成测试和文档测试,并用cargo doc生成漂亮的API文档。

🧪 单元测试

// 测试函数标注 #[test]
// 通常放在同一文件中,用 #[cfg(test)] 模块包裹

fn add(a: i32, b: i32) -> i32 { a + b }
fn multiply(a: i32, b: i32) -> i32 { a * b }
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 { Err("除零错误".to_string()) } else { Ok(a / b) }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
        assert_eq!(add(-1, 1), 0);
        assert_eq!(add(0, 0), 0);
    }
    
    #[test]
    fn test_multiply() {
        assert_eq!(multiply(3, 4), 12);
        assert_eq!(multiply(-2, 3), -6);
    }
    
    #[test]
    fn test_divide_ok() {
        assert_eq!(divide(10.0, 2.0), Ok(5.0));
    }
    
    #[test]
    fn test_divide_zero() {
        assert!(divide(1.0, 0.0).is_err());
        assert_eq!(divide(1.0, 0.0), Err("除零错误".to_string()));
    }
    
    #[test]
    #[should_panic(expected = "overflow")]
    fn test_overflow() {
        // 测试应该panic的情况
        let _ = i32::MAX + 1;  // debug模式会panic
    }
    
    #[test]
    fn test_ignore() -> Result<(), String> {
        // 使用?运算符的测试
        let result = divide(10.0, 3.0)?;
        assert!((result - 3.333).abs() < 0.01);
        Ok(())
    }
}

fn main() {
    println!("运行测试: cargo test");
    println!("测试单个: cargo test test_add");
    println!("显示输出: cargo test -- --show-output");
    println!("忽略测试: cargo test -- --ignored");
}
$ cargo test running 5 tests test tests::test_add ... ok test tests::test_multiply ... ok test tests::test_divide_ok ... ok test tests::test_divide_zero ... ok test tests::test_ignore ... ok test result: ok. 5 passed; 0 failed

✅ 验证通过

📚 文档注释与cargo doc

/// 计算两个整数的和
/// 
/// # 参数
/// - `a` - 第一个加数
/// - `b` - 第二个加数
/// 
/// # 返回值
/// 两个数的和
/// 
/// # 示例
/// ```
/// let result = mylib::add(2, 3);
/// assert_eq!(result, 5);
/// ```
fn add(a: i32, b: i32) -> i32 { a + b }

//! 模块级文档(在文件开头)
//! 这个库提供了数学计算工具

// 生成文档: cargo doc
// 打开文档: cargo doc --open

🏗️ 综合实战:完整的测试套件

pub struct Calculator {
    history: Vec<String>,
}

impl Calculator {
    pub fn new() -> Self { Calculator { history: Vec::new() } }
    
    pub fn add(&mut self, a: f64, b: f64) -> f64 {
        let result = a + b;
        self.history.push(format!("{} + {} = {}", a, b, result));
        result
    }
    
    pub fn subtract(&mut self, a: f64, b: f64) -> f64 {
        let result = a - b;
        self.history.push(format!("{} - {} = {}", a, b, result));
        result
    }
    
    pub fn multiply(&mut self, a: f64, b: f64) -> f64 {
        let result = a * b;
        self.history.push(format!("{} * {} = {}", a, b, result));
        result
    }
    
    pub fn divide(&mut self, a: f64, b: f64) -> Result<f64, String> {
        if b == 0.0 { return Err("不能除零".into()); }
        let result = a / b;
        self.history.push(format!("{} / {} = {}", a, b, result));
        Ok(result)
    }
    
    pub fn history(&self) -> &[String] { &self.history }
    pub fn clear(&mut self) { self.history.clear(); }
}

fn main() {
    let mut calc = Calculator::new();
    println!("2 + 3 = {}", calc.add(2.0, 3.0));
    println!("10 / 4 = {:?}", calc.divide(10.0, 4.0));
    println!("历史: {:?}", calc.history());
}
2 + 3 = 5 10 / 4 = Ok(2.5) 历史: ["2 + 3 = 5", "10 / 4 = 2.5"]

✅ 验证通过

📝 练习

练习1:TDD开发

先写测试,再实现函数:is_palindrome(s: &str) -> bool

练习2:基准测试

为排序算法编写基准测试,比较冒泡排序和内置排序的性能。

🏆 本课成就

🔧 测试进阶

fn fibonacci(n: u32) -> u64 {
    let mut a = 0u64; let mut b = 1u64;
    for _ in 0..n { let t = a + b; a = b; b = t; }
    a
}

fn is_prime(n: u64) -> bool {
    if n < 2 { return false; }
    if n < 4 { return true; }
    if n % 2 == 0 || n % 3 == 0 { return false; }
    let mut i = 5;
    while i * i <= n { if n % i == 0 || n % (i+2) == 0 { return false; } i += 6; }
    true
}

fn main() {
    println!("fib(50) = {}", fibonacci(50));
    let primes: Vec = (2..100).filter(|&n| is_prime(n)).collect();
    println!("100以内素数({}个): {:?}", primes.len(), primes);
    println!("运行测试: cargo test");
    println!("文档测试: cargo test --doc");
    println!("基准测试: cargo bench");
}
fib(50) = 12586269025 100以内素数(25个): [2, 3, 5, 7, 11, ...] 运行测试: cargo test

✅ 验证通过

📊 测试策略

// 测试金字塔
//       /  E2E  \
//      / 集成测试 \
//     /  单元测试   \
//    /_______________\

// 单元测试: 测试单个函数/方法
// 集成测试: tests/目录,测试模块间交互
// E2E测试: 测试完整用户流程

// 测试辅助工具
fn fibonacci(n: u32) -> u64 {
    let mut a = 0u64; let mut b = 1u64;
    for _ in 0..n { let t = a + b; a = b; b = t; }
    a
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_fibonacci_base() {
        assert_eq!(fibonacci(0), 0);
        assert_eq!(fibonacci(1), 1);
        assert_eq!(fibonacci(2), 1);
    }
    
    #[test]
    fn test_fibonacci_values() {
        assert_eq!(fibonacci(10), 55);
        assert_eq!(fibonacci(20), 6765);
    }
    
    // 参数化测试模式
    #[test]
    fn test_fibonacci_table() {
        let cases = [(0,0),(1,1),(5,5),(10,55),(20,6765)];
        for (input, expected) in cases {
            assert_eq!(fibonacci(input), expected, "fib({})", input);
        }
    }
}

fn main() {
    println!("测试金字塔: 单元 > 集成 > E2E");
    println!("运行: cargo test");
    println!("详细: cargo test -- --show-output");
    println!("单个: cargo test test_fibonacci_base");
    println!("并行: cargo test -- --test-threads=4");
    println!("文档测试: cargo test --doc");
}

🔧 文档最佳实践

/// 计算斐波那契数
/// 
/// # 参数
/// * `n` - 非负整数
/// 
/// # 返回
/// 第n个斐波那契数
/// 
/// # Panics
/// 当n过大导致u64溢出时
/// 
/// # Examples
/// ```
/// use mylib::fibonacci;
/// assert_eq!(fibonacci(10), 55);
/// ```
fn fibonacci(n: u32) -> u64 { /* ... */ }

// cargo doc --open 生成并打开文档
// 文档包含: 搜索、模块树、源码链接

🏗️ 综合实战:测试驱动的栈实现

pub struct Stack {
    items: Vec,
}

impl Stack {
    pub fn new() -> Self { Stack { items: Vec::new() } }
    pub fn with_capacity(capacity: usize) -> Self {
        Stack { items: Vec::with_capacity(capacity) }
    }
    pub fn push(&mut self, item: T) { self.items.push(item); }
    pub fn pop(&mut self) -> Option { self.items.pop() }
    pub fn peek(&self) -> Option<&T> { self.items.last() }
    pub fn peek_mut(&mut self) -> Option<&mut T> { self.items.last_mut() }
    pub fn len(&self) -> usize { self.items.len() }
    pub fn is_empty(&self) -> bool { self.items.is_empty() }
    pub fn clear(&mut self) { self.items.clear(); }
    pub fn into_vec(self) -> Vec { self.items }
}

impl Stack {
    pub fn peek_all(&self) -> &[T] { &self.items }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_new_stack() {
        let s: Stack = Stack::new();
        assert!(s.is_empty());
        assert_eq!(s.len(), 0);
    }
    
    #[test]
    fn test_push_pop() {
        let mut s = Stack::new();
        s.push(1); s.push(2); s.push(3);
        assert_eq!(s.len(), 3);
        assert_eq!(s.pop(), Some(3));
        assert_eq!(s.pop(), Some(2));
        assert_eq!(s.peek(), Some(&1));
    }
    
    #[test]
    fn test_empty_pop() {
        let mut s: Stack = Stack::new();
        assert_eq!(s.pop(), None);
    }
    
    #[test]
    fn test_peek_mut() {
        let mut s = Stack::new();
        s.push(10);
        if let Some(top) = s.peek_mut() { *top += 5; }
        assert_eq!(s.pop(), Some(15));
    }
    
    #[test]
    fn test_clear() {
        let mut s = Stack::new();
        s.push(1); s.push(2);
        s.clear();
        assert!(s.is_empty());
    }
}

fn main() {
    let mut stack = Stack::new();
    for i in 1..=5 { stack.push(i * 10); }
    println!("栈内容: {:?}", stack.peek_all());
    while let Some(v) = stack.pop() { print!("{} ", v); }
    println!();
}
栈内容: [10, 20, 30, 40, 50] 50 40 30 20 10

✅ 验证通过

📋 测试清单

单元测试检查清单

集成测试检查清单

🔧 持续集成配置

# .github/workflows/ci.yml
# name: Rust CI
# on: [push, pull_request]
# jobs:
#   test:
#     runs-on: ubuntu-latest
#     steps:
#       - uses: actions/checkout@v4
#       - run: rustup toolchain install stable
#       - run: cargo build --verbose
#       - run: cargo test --verbose
#       - run: cargo clippy -- -D warnings
#       - run: cargo fmt --check
#       - run: cargo doc --no-deps

fn main() {
    println!("CI最佳实践:");
    println!("  1. 每次提交都运行测试");
    println!("  2. clippy检查代码质量");
    println!("  3. rustfmt检查代码格式");
    println!("  4. cargo doc确保文档完整");
    println!("  5. 覆盖率: cargo tarpaulin");
    println!("  6. 安全审计: cargo audit");
}

🏗️ 测试实战:属性测试

// 属性测试(Property-based Testing)
// 不测试具体值,而是测试属性(规律)

fn reverse_string(s: &str) -> String { s.chars().rev().collect() }
fn sort_vec(v: &mut Vec) { v.sort(); }

fn main() {
    // 属性1: 反转两次等于原值
    println!("属性测试: 反转两次 = 原值");
    for test in ["hello", "Rust🦀", "abba", ""] {
        let double_reversed = reverse_string(&reverse_string(test));
        assert_eq!(test, double_reversed, "反转两次应等于原值");
    }
    println!("  ✅ 通过");
    
    // 属性2: 排序后长度不变
    println!("属性测试: 排序后长度不变");
    for _ in 0..10 {
        let mut v: Vec = (0..20).map(|_| rand_val()).collect();
        let len_before = v.len();
        sort_vec(&mut v);
        assert_eq!(v.len(), len_before);
    }
    println!("  ✅ 通过");
    
    // 属性3: 排序后有序
    println!("属性测试: 排序后有序");
    let mut v = vec![5, 2, 8, 1, 9];
    sort_vec(&mut v);
    for i in 0..v.len()-1 { assert!(v[i] <= v[i+1]); }
    println!("  ✅ {:?} 有序", v);
}

fn rand_val() -> i32 { (std::time::SystemTime::now().elapsed().unwrap().subsec_nanos() % 100) as i32 }
属性测试: 反转两次 = 原值 ✅ 通过 属性测试: 排序后长度不变 ✅ 通过 属性测试: 排序后有序 ✅ [1, 2, 5, 8, 9] 有序

✅ 验证通过