第十八章:测试与文档——质量的守护者

第十八章:测试与文档——质量的守护者

本章导读:代码不仅要能运行,还要能正确运行。测试是保证代码质量的护城河,文档是帮助他人(和未来的自己)理解代码的灯塔。Rust 提供了一流的测试框架和文档系统,让测试和文档成为代码的一等公民。


🧪 18.1 单元测试

🎯 18.1.1 基本测试

// src/lib.rs
/// 计算两个数的和
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// 计算两个数的差
pub fn subtract(a: i32, b: i32) -> i32 {
    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_subtract() {
        assert_eq!(subtract(5, 3), 2);
        assert_eq!(subtract(0, 5), -5);
    }

    #[test]
    fn test_add_overflow() {
        // 使用 assert! 进行布尔断言
        assert!(add(i32::MAX, 1) < 0);  // 溢出回绕
    }
}

运行测试:

cargo test

🔍 18.1.2 断言宏

#[cfg(test)]
mod tests {
    #[test]
    fn test_assertions() {
        // 相等断言
        assert_eq!(1 + 1, 2, "1 + 1 应该等于 2");
        assert_ne!(1 + 1, 3, "1 + 1 不应该等于 3");

        // 布尔断言
        assert!(true, "true 应该是 true");

        // 结果断言(返回 Result)
        let result: Result<(), String> = Ok(());
        assert!(result.is_ok());

        // 恐慌断言
        // assert!(expr, "可选的错误消息");
    }

    #[test]
    #[should_panic(expected = "division by zero")]  // 期望发生 panic
    fn test_divide_by_zero() {
        divide(1, 0);  // 应该 panic
    }

    #[test]
    fn test_result() -> Result<(), String> {
        // 返回 Result 的测试
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("数学出错了"))
        }
    }
}

fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("division by zero");
    }
    a / b
}

🔬 18.1.3 测试私有函数

// 私有函数
fn internal_add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_internal() {
        // 在同一模块中可以直接测试私有函数
        assert_eq!(internal_add(1, 2), 3);
    }
}

🎭 18.2 集成测试

📁 18.2.1 tests 目录

my_project/
├── Cargo.toml
├── src/
│   └── lib.rs
└── tests/
    ├── integration_test.rs
    └── common/
        └── mod.rs
// tests/integration_test.rs
use my_project::{add, subtract};

#[test]
fn test_add_integration() {
    assert_eq!(add(100, 200), 300);
}

#[test]
fn test_subtract_integration() {
    assert_eq!(subtract(10, 5), 5);
}

📦 18.2.2 共享测试模块

// tests/common/mod.rs
pub fn setup() {
    // 测试前初始化
    println!("设置测试环境...");
}

pub fn teardown() {
    // 测试后清理
    println!("清理测试环境...");
}
// tests/integration_test.rs
mod common;

use my_project::add;

#[test]
fn test_with_setup() {
    common::setup();
    assert_eq!(add(1, 2), 3);
    common::teardown();
}

📖 18.3 文档测试

📝 18.3.1 文档中的代码块

/// 计算斐波那契数
///
/// # Examples
///
/// ```
/// use my_project::fibonacci;
///
/// assert_eq!(fibonacci(0), 0);
/// assert_eq!(fibonacci(1), 1);
/// assert_eq!(fibonacci(10), 55);
/// ```
///
/// # Panics
///
/// 当输入为负数时会 panic
///
/// ```should_panic
/// use my_project::fibonacci;
/// fibonacci(-1);  // panic!
/// ```
pub fn fibonacci(n: i32) -> i64 {
    if n < 0 {
        panic!("n must be non-negative");
    }
    if n <= 1 {
        n as i64
    } else {
        fibonacci(n - 1) + fibonacci(n - 2)
    }
}

🔧 18.3.2 文档测试属性

/// 使用外部 crate
///
/// ```ignore
/// // 完全忽略此测试
/// use some_unavailable_crate;
/// ```
///
/// ```no_run
/// // 编译但不运行
/// use my_project::long_running;
/// long_running();  // 不会执行
/// ```
///
/// ```compile_fail
/// // 期望编译失败
/// use my_project::typed_function;
/// typed_function("wrong type");  // 类型错误
/// ```

🏃 18.4 测试组织

🏷️ 18.4.1 测试分类

#[cfg(test)]
mod tests {
    use super::*;

    // 单元测试:快速、隔离
    #[test]
    fn unit_test() {
        assert!(true);
    }

    // 集成测试:慢、需要外部资源
    #[test]
    #[ignore]  // 默认忽略
    fn integration_test() {
        // 需要 HTTP 服务
    }

    // 运行被忽略的测试
    // cargo test -- --ignored
}

🔢 18.4.2 运行特定测试

# 运行名称包含 "add" 的测试
cargo test add

# 运行特定测试
cargo test test_add

# 运行被忽略的测试
cargo test -- --ignored

# 并行运行测试(默认)
cargo test -- --test-threads=4

# 顺序运行测试
cargo test -- --test-threads=1

# 显示输出
cargo test -- --nocapture

🎨 18.5 基准测试

⏱️ 18.5.1 简单计时

#[cfg(test)]
mod bench {
    use super::*;
    use std::time::Instant;

    #[test]
    fn bench_fibonacci() {
        let start = Instant::now();

        for _ in 0..1000 {
            fibonacci(20);
        }

        let duration = start.elapsed();
        println!("1000 次 fibonacci(20) 耗时: {:?}", duration);
    }
}

📊 18.5.2 使用 criterion

# Cargo.toml
[dev-dependencies]
criterion = "0.5"

[[bench]]
name = "fibonacci_bench"
harness = false
// benches/fibonacci_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use my_project::fibonacci;

fn fibonacci_benchmark(c: &mut Criterion) {
    c.bench_function("fibonacci 20", |b| {
        b.iter(|| fibonacci(black_box(20)))
    });

    c.bench_function("fibonacci 30", |b| {
        b.iter(|| fibonacci(black_box(30)))
    });
}

criterion_group!(benches, fibonacci_benchmark);
criterion_main!(benches);

运行基准测试:

cargo bench

📚 18.6 文档系统

📖 18.6.1 文档注释

/// 用户结构体
///
/// 表示系统中的一个用户。
///
/// # Examples
///
/// 创建一个新用户:
///
/// ```
/// use my_project::User;
///
/// let user = User::new(1, "Alice");
/// assert_eq!(user.name(), "Alice");
/// ```
///
/// # Fields
///
/// - `id`: 用户的唯一标识符
/// - `name`: 用户名
pub struct User {
    id: u64,
    name: String,
}

impl User {
    /// 创建新用户
    ///
    /// # Arguments
    ///
    /// * `id` - 用户 ID
    /// * `name` - 用户名
    ///
    /// # Returns
    ///
    /// 返回新的 `User` 实例
    ///
    /// # Examples
    ///
    /// ```
    /// use my_project::User;
    ///
    /// let user = User::new(1, "Alice");
    /// ```
    pub fn new(id: u64, name: &str) -> Self {
        Self {
            id,
            name: name.to_string(),
        }
    }

    /// 获取用户名
    ///
    /// 返回用户名的引用
    pub fn name(&self) -> &str {
        &self.name
    }

    /// 获取用户 ID
    pub fn id(&self) -> u64 {
        self.id
    }
}

🔗 18.6.2 文档链接

/// 文档链接示例
///
/// 链接到标准库:[`Vec`]
///
/// 链接到项目内项:[`User`]
///
/// 链接到方法:[`User::name`]
///
/// 外部链接:[`serde::Serialize`]
pub struct Documented {
    /// 内部字段,链接到 [`User`]
    user: User,
}

/// 参见 [`std::collections::HashMap`]
pub fn use_hashmap() {}

🎯 18.6.3 文档属性

/// 隐藏此项(不在文档中显示)
#[doc(hidden)]
pub fn internal_api() {}

/// 将此项别名为另一个路径
#[doc(alias = "create_user")]
pub fn new_user() {}

/// 指定文档在哪些功能下可见
#[cfg(feature = "advanced")]
#[doc(cfg(feature = "advanced"))]
pub fn advanced_feature() {}

🌐 18.7 生成文档

📖 18.7.1 生成 HTML 文档

# 生成文档
cargo doc

# 生成并打开
cargo doc --open

# 包含依赖文档
cargo doc --document-private-items

📝 18.7.2 文档首页

// src/lib.rs

//! # My Project
//!
//! `my_project` 是一个演示文档的库。
//!
//! ## Features
//!
//! - Feature 1
//! - Feature 2
//!
//! ## Quick Start
//!
//! ```
//! use my_project::add;
//! assert_eq!(add(1, 2), 3);
//! ```

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

🧪 18.8 测试实战:一个完整示例

/// 简易计算器
pub struct Calculator {
    result: f64,
}

impl Calculator {
    /// 创建新计算器
    ///
    /// # Examples
    ///
    /// ```
    /// use my_project::Calculator;
    /// let calc = Calculator::new();
    /// assert_eq!(calc.result(), 0.0);
    /// ```
    pub fn new() -> Self {
        Self { result: 0.0 }
    }

    /// 加法
    ///
    /// # Examples
    ///
    /// ```
    /// use my_project::Calculator;
    /// let mut calc = Calculator::new();
    /// calc.add(5.0);
    /// assert_eq!(calc.result(), 5.0);
    /// ```
    pub fn add(&mut self, value: f64) -> &mut Self {
        self.result += value;
        self
    }

    /// 减法
    pub fn subtract(&mut self, value: f64) -> &mut Self {
        self.result -= value;
        self
    }

    /// 乘法
    pub fn multiply(&mut self, value: f64) -> &mut Self {
        self.result *= value;
        self
    }

    /// 除法
    ///
    /// # Errors
    ///
    /// 除以零时返回错误
    ///
    /// # Examples
    ///
    /// ```
    /// use my_project::Calculator;
    /// let mut calc = Calculator::new();
    /// calc.add(10.0);
    /// calc.divide(2.0).unwrap();
    /// assert_eq!(calc.result(), 5.0);
    /// ```
    pub fn divide(&mut self, value: f64) -> Result<&mut Self, String> {
        if value == 0.0 {
            return Err("除数不能为零".to_string());
        }
        self.result /= value;
        Ok(self)
    }

    /// 获取结果
    pub fn result(&self) -> f64 {
        self.result
    }

    /// 重置
    pub fn reset(&mut self) -> &mut Self {
        self.result = 0.0;
        self
    }
}

impl Default for Calculator {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new() {
        let calc = Calculator::new();
        assert_eq!(calc.result(), 0.0);
    }

    #[test]
    fn test_add() {
        let mut calc = Calculator::new();
        calc.add(5.0).add(3.0);
        assert_eq!(calc.result(), 8.0);
    }

    #[test]
    fn test_subtract() {
        let mut calc = Calculator::new();
        calc.add(10.0).subtract(3.0);
        assert_eq!(calc.result(), 7.0);
    }

    #[test]
    fn test_multiply() {
        let mut calc = Calculator::new();
        calc.add(5.0).multiply(2.0);
        assert_eq!(calc.result(), 10.0);
    }

    #[test]
    fn test_divide() {
        let mut calc = Calculator::new();
        calc.add(10.0);
        calc.divide(2.0).unwrap();
        assert_eq!(calc.result(), 5.0);
    }

    #[test]
    fn test_divide_by_zero() {
        let mut calc = Calculator::new();
        calc.add(10.0);
        let result = calc.divide(0.0);
        assert!(result.is_err());
    }

    #[test]
    fn test_chain_operations() {
        let mut calc = Calculator::new();
        calc.add(10.0)
           .multiply(2.0)
           .subtract(5.0)
           .divide(3.0)
           .unwrap();
        // (10 * 2 - 5) / 3 = 5
        assert_eq!(calc.result(), 5.0);
    }

    #[test]
    fn test_reset() {
        let mut calc = Calculator::new();
        calc.add(100.0).reset();
        assert_eq!(calc.result(), 0.0);
    }
}

📝 本章小结

本章我们学习了 Rust 的测试与文档系统:

测试类型用途位置
单元测试测试内部逻辑src/#[cfg(test)]
集成测试测试公共 APItests/ 目录
文档测试示例即测试文档注释中
文档元素语法
模块文档//!
项文档///
代码块 `
链接[Item]

关键原则:

  • 测试要快、独立、可重复
  • 文档要有示例、说明边界条件
  • 保持文档与代码同步

动手实验

  1. 为之前的 Calculator 添加 pow(幂运算)方法,并编写测试。
  2. 创建一个 tests/ 目录,编写集成测试。
  3. 为你的库添加完整的文档注释,包括 Examples、Panics、Errors 等。
  4. 使用 criterion 为 fibonacci 函数编写基准测试。
← 返回目录