Rust从入门到精通之进阶篇:18.测试与文档

测试与文档

编写测试和文档是开发高质量软件的关键部分。Rust 提供了强大的测试框架和文档生成工具,使开发者能够确保代码的正确性并提供清晰的使用指南。在本章中,我们将探索 Rust 的测试系统和文档工具。

Rust 的测试哲学

Rust 的测试系统内置于语言和构建工具中,鼓励开发者将测试作为开发过程的一部分。Rust 支持多种测试类型:

  1. 单元测试:测试单个函数或模块的功能
  2. 集成测试:测试多个部分如何协同工作
  3. 文档测试:确保文档中的代码示例是正确的

单元测试

单元测试通常与被测试的代码放在同一个文件中,使用 #[cfg(test)] 属性标记测试模块。

创建测试模块

rust 复制代码
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

#[cfg(test)] 属性告诉 Rust 只在执行 cargo test 命令时才编译和运行测试代码。

测试函数

测试函数使用 #[test] 属性标记:

rust 复制代码
#[test]
fn it_adds_two() {
    assert_eq!(4, add(2, 2));
}

断言宏

Rust 提供了多种断言宏用于测试:

  • assert!:断言一个布尔表达式为 true
  • assert_eq!:断言两个值相等
  • assert_ne!:断言两个值不相等
rust 复制代码
#[test]
fn test_assertions() {
    // 断言表达式为 true
    assert!(1 < 2);
    
    // 断言两个值相等
    assert_eq!(4, add(2, 2));
    
    // 断言两个值不相等
    assert_ne!(5, add(2, 2));
    
    // 添加自定义错误消息
    assert!(
        add(2, 2) == 4,
        "加法函数返回了 {}, 而不是 4",
        add(2, 2)
    );
}

测试 panic

有时我们需要测试函数在特定条件下是否会 panic:

rust 复制代码
pub fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("除数不能为零");
    }
    a / b
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_divide() {
        assert_eq!(divide(10, 2), 5);
    }
    
    #[test]
    #[should_panic(expected = "除数不能为零")]
    fn test_divide_by_zero() {
        divide(10, 0);
    }
}

#[should_panic] 属性表示测试应该导致 panic。可以使用 expected 参数指定期望的 panic 消息。

使用 Result 类型

测试函数也可以返回 Result<(), E> 类型,这样可以使用 ? 运算符:

rust 复制代码
#[test]
fn test_with_result() -> Result<(), String> {
    if add(2, 2) == 4 {
        Ok(())
    } else {
        Err(String::from("加法函数返回了错误的值"))
    }
}

控制测试执行

cargo test 命令提供了多种选项来控制测试执行:

bash 复制代码
# 运行所有测试
cargo test

# 运行特定测试
cargo test test_divide

# 运行包含特定字符串的测试
cargo test divide

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

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

# 串行运行测试
cargo test -- --test-threads=1

集成测试

集成测试位于项目根目录的 tests 文件夹中,与源代码分开。它们测试库的公共 API,就像外部代码使用它一样。

创建集成测试

复制代码
my_project/
├── Cargo.toml
├── src/
│   └── lib.rs
└── tests/
    └── integration_test.rs
rust 复制代码
// tests/integration_test.rs
use my_project; // 导入库 crate

#[test]
fn test_add() {
    assert_eq!(4, my_project::add(2, 2));
}

共享测试代码

如果需要在多个集成测试文件之间共享代码,可以创建一个 tests/common 模块:

复制代码
my_project/
├── Cargo.toml
├── src/
│   └── lib.rs
└── tests/
    ├── common/
    │   └── mod.rs
    └── integration_test.rs
rust 复制代码
// tests/common/mod.rs
pub fn setup() {
    // 设置测试环境
}
rust 复制代码
// tests/integration_test.rs
mod common;

#[test]
fn test_with_setup() {
    common::setup();
    // 测试代码
}

文档测试

Rust 的文档注释中的代码示例可以作为测试运行,确保文档与代码保持同步。

文档注释

Rust 支持两种文档注释:

  • ///:为下面的项生成文档
  • //!:为包含注释的项生成文档
rust 复制代码
/// 将两个数字相加
///
/// # 示例
///
/// ```
/// let result = my_crate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

//! # My Crate
//!
//! `my_crate` 是一个实用工具集合

运行文档测试

bash 复制代码
# 运行所有测试,包括文档测试
cargo test

# 只运行文档测试
cargo test --doc

隐藏测试代码

有时你可能需要在文档中包含一些不应该显示在生成的文档中的测试代码:

rust 复制代码
/// 将两个数字相加
///
/// # 示例
///
/// ```
/// # use my_crate::add;
/// let result = add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

# 开头的行在生成的文档中不会显示,但在测试中会执行。

指定测试行为

可以使用特殊注释控制文档测试的行为:

rust 复制代码
/// ```
/// let result = add(2, 2);
/// assert_eq!(result, 4);
/// ```
///
/// ```should_panic
/// // 这个测试应该 panic
/// add(1, 0);
/// ```
///
/// ```no_run
/// // 这个代码不会运行,但会编译检查
/// let file = std::fs::File::open("不存在的文件").unwrap();
/// ```
///
/// ```ignore
/// // 这个代码完全被忽略
/// let x = 不会编译的代码;
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

生成文档

Rust 的文档工具 rustdoc 可以从代码和注释生成 HTML 文档。

生成和查看文档

bash 复制代码
# 生成文档
cargo doc

# 生成文档并打开浏览器
cargo doc --open

# 包含私有项
cargo doc --document-private-items

文档格式

文档注释支持 Markdown 格式:

rust 复制代码
/// # 向量工具
///
/// 提供用于处理向量的实用函数。
///
/// ## 功能
///
/// - 向量求和
/// - 向量平均值
/// - 向量标准差
///
/// ## 示例
///
/// ```
/// use vector_utils::sum;
///
/// let v = vec![1, 2, 3, 4, 5];
/// assert_eq!(sum(&v), 15);
/// ```
pub fn sum(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

常用文档部分

文档通常包含以下部分:

  • 示例(Examples):如何使用函数或类型
  • 恐慌(Panics):函数可能会恐慌的情况
  • 错误(Errors):函数可能返回的错误
  • 安全性 (Safety):使用 unsafe 函数的注意事项
rust 复制代码
/// 从向量中获取指定索引的元素
///
/// # 示例
///
/// ```
/// use my_crate::get;
/// let v = vec![10, 20, 30];
/// assert_eq!(get(&v, 1), Some(&20));
/// assert_eq!(get(&v, 3), None);
/// ```
///
/// # 恐慌
///
/// 如果索引超出范围且 `panic_on_out_of_bounds` 为 true,则会恐慌。
///
/// # 安全性
///
/// 此函数不使用 `unsafe` 代码。
pub fn get<T>(slice: &[T], index: usize) -> Option<&T> {
    if index < slice.len() {
        Some(&slice[index])
    } else {
        None
    }
}

基准测试

Rust 的 nightly 版本提供了基准测试功能,用于测量代码性能。在稳定版中,可以使用 criterion 等第三方库。

使用 Criterion

首先,添加 criterion 依赖:

toml 复制代码
# Cargo.toml
[dev-dependencies]
criterion = "0.3"

[[bench]]
name = "my_benchmark"
harness = false

然后创建基准测试:

rust 复制代码
// benches/my_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use my_crate::fibonacci;

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

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

运行基准测试:

bash 复制代码
cargo bench

属性测试

属性测试(也称为基于属性的测试或模糊测试)使用随机生成的输入测试代码。

使用 proptest

添加 proptest 依赖:

toml 复制代码
# Cargo.toml
[dev-dependencies]
proptest = "1.0"

创建属性测试:

rust 复制代码
#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;
    
    proptest! {
        #[test]
        fn test_add_commutative(a in -1000..1000, b in -1000..1000) {
            assert_eq!(add(a, b), add(b, a));
        }
        
        #[test]
        fn test_add_associative(a in -1000..1000, b in -1000..1000, c in -1000..1000) {
            assert_eq!(add(add(a, b), c), add(a, add(b, c)));
        }
    }
}

测试最佳实践

1. 测试驱动开发 (TDD)

测试驱动开发是一种先编写测试,然后实现功能的方法:

  1. 编写一个失败的测试
  2. 实现最小代码使测试通过
  3. 重构代码,保持测试通过
  4. 重复上述步骤
rust 复制代码
// 第一步:编写测试
#[test]
fn test_is_prime() {
    assert!(!is_prime(1));
    assert!(is_prime(2));
    assert!(is_prime(3));
    assert!(!is_prime(4));
    assert!(is_prime(5));
    assert!(!is_prime(6));
}

// 第二步:实现功能
pub fn is_prime(n: u64) -> bool {
    if n <= 1 {
        return false;
    }
    if n <= 3 {
        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
}

2. 测试覆盖率

使用 grcovtarpaulin 工具测量代码覆盖率:

bash 复制代码
# 安装 tarpaulin
cargo install cargo-tarpaulin

# 运行覆盖率分析
cargo tarpaulin

3. 模拟和测试替身

使用 mockall 库创建模拟对象:

toml 复制代码
# Cargo.toml
[dev-dependencies]
mockall = "0.11"
rust 复制代码
use mockall::predicate::*;
use mockall::*;

#[automock]
trait Database {
    fn get_user(&self, id: u32) -> Option<String>;
    fn save_user(&self, id: u32, name: &str) -> bool;
}

struct UserService<T: Database> {
    database: T,
}

impl<T: Database> UserService<T> {
    fn new(database: T) -> Self {
        Self { database }
    }
    
    fn get_user_name(&self, id: u32) -> String {
        self.database.get_user(id).unwrap_or_else(|| String::from("Guest"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_get_user_name_existing() {
        let mut mock_db = MockDatabase::new();
        mock_db.expect_get_user()
            .with(eq(1))
            .times(1)
            .returning(|_| Some(String::from("Alice")));
        
        let service = UserService::new(mock_db);
        assert_eq!(service.get_user_name(1), "Alice");
    }
    
    #[test]
    fn test_get_user_name_not_found() {
        let mut mock_db = MockDatabase::new();
        mock_db.expect_get_user()
            .with(eq(999))
            .times(1)
            .returning(|_| None);
        
        let service = UserService::new(mock_db);
        assert_eq!(service.get_user_name(999), "Guest");
    }
}

4. 测试私有函数

通常,我们只测试公共 API。但如果需要测试私有函数,有几种方法:

rust 复制代码
// 方法 1:在测试模块中使用 super::*
mod math {
    fn is_even(n: i32) -> bool {
        n % 2 == 0
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        
        #[test]
        fn test_is_even() {
            assert!(is_even(2));
            assert!(!is_even(3));
        }
    }
}

// 方法 2:使用 #[cfg(test)] 路径
pub mod utils {
    fn internal_helper(x: i32) -> i32 {
        x * 2
    }
    
    pub fn double(x: i32) -> i32 {
        internal_helper(x)
    }
    
    // 仅在测试时公开
    #[cfg(test)]
    pub(crate) use self::internal_helper;
}

#[cfg(test)]
mod tests {
    use super::utils::internal_helper;
    
    #[test]
    fn test_internal_helper() {
        assert_eq!(internal_helper(5), 10);
    }
}

5. 测试组织

随着测试数量增加,可以使用嵌套模块组织测试:

rust 复制代码
#[cfg(test)]
mod tests {
    use super::*;
    
    mod validation {
        use super::*;
        
        #[test]
        fn test_validate_username() {
            // 测试用户名验证
        }
        
        #[test]
        fn test_validate_email() {
            // 测试电子邮件验证
        }
    }
    
    mod calculation {
        use super::*;
        
        #[test]
        fn test_calculate_total() {
            // 测试总计计算
        }
        
        #[test]
        fn test_calculate_tax() {
            // 测试税款计算
        }
    }
}

文档最佳实践

1. 文档结构

良好的文档应该包含:

  • 简短的摘要(第一行)
  • 详细描述
  • 参数和返回值说明
  • 示例代码
  • 错误处理说明
  • 相关函数链接
rust 复制代码
/// 计算两个数字的商。
///
/// 返回 `a` 除以 `b` 的结果。如果 `b` 为零,返回 `None`。
///
/// # 参数
///
/// * `a` - 被除数
/// * `b` - 除数
///
/// # 返回值
///
/// 如果 `b` 不为零,返回 `Some(a / b)`;否则返回 `None`。
///
/// # 示例
///
/// ```
/// use my_crate::safe_divide;
///
/// assert_eq!(safe_divide(10, 2), Some(5));
/// assert_eq!(safe_divide(10, 0), None);
/// ```
///
/// # 相关函数
///
/// * [`safe_remainder`] - 计算安全的余数
pub fn safe_divide(a: i32, b: i32) -> Option<i32> {
    if b == 0 {
        None
    } else {
        Some(a / b)
    }
}

2. 模块级文档

使用 //! 为模块添加文档:

rust 复制代码
//! # 数学工具模块
//!
//! 提供各种数学运算函数。
//!
//! ## 功能
//!
//! - 基本算术运算
//! - 安全除法和余数计算
//! - 数值转换工具

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

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

3. 链接和交叉引用

在文档中使用链接:

rust 复制代码
/// 参见 [`add`] 函数了解加法操作。
///
/// 或者查看 [crate::math::complex] 模块了解复数运算。
///
/// 外部链接:[Rust 文档](https://doc.rust-lang.org/)
pub fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

4. 文档中的代码块

在文档中使用不同类型的代码块:

rust 复制代码
/// 示例代码:
///
/// ```
/// // Rust 代码
/// let x = 5;
/// ```
///
/// ```text
/// 纯文本输出
/// Hello, world!
/// ```
///
/// ```json
/// {"name": "Alice", "age": 30}
/// ```
pub fn example() {}

5. 条件编译属性

使用条件编译为不同平台提供不同文档:

rust 复制代码
/// 读取文件内容。
///
/// # 示例
///
#[cfg(unix)]
/// ```
/// // Unix 示例
/// use std::fs;
/// let content = fs::read_to_string("/etc/passwd").unwrap();
/// ```
///
#[cfg(windows)]
/// ```
/// // Windows 示例
/// use std::fs;
/// let content = fs::read_to_string("C:\\Windows\\System32\\drivers\\etc\\hosts").unwrap();
/// ```
pub fn read_file(path: &str) -> std::io::Result<String> {
    std::fs::read_to_string(path)
}

练习题

  1. 为一个简单的计算器库编写单元测试,测试加、减、乘、除四种基本运算。确保除法函数在除数为零时正确处理错误。

  2. 创建一个字符串处理库,包含至少三个函数(如反转字符串、计算单词数、转换大小写等)。为每个函数编写文档注释,包括示例代码,并确保文档测试能够通过。

  3. 实现一个简单的栈数据结构,并为其编写集成测试。测试应该覆盖压栈、弹栈、查看栈顶元素和检查栈是否为空等操作。

  4. 使用属性测试(如 proptest)为一个排序函数编写测试,验证排序后的数组满足以下属性:元素已排序、元素数量不变、所有原始元素都存在。

  5. 为一个现有的 Rust 项目添加基准测试,比较至少两种不同实现的性能。使用 Criterion 库记录和可视化性能结果。

总结

在本章中,我们探讨了 Rust 的测试和文档系统:

  • 单元测试、集成测试和文档测试的编写和运行
  • 使用断言宏和 should_panic 属性测试代码行为
  • 基准测试和属性测试的基础知识
  • 编写清晰、全面的文档注释
  • 生成和组织 API 文档
  • 测试和文档的最佳实践

测试和文档是高质量软件开发的关键部分。Rust 的内置测试框架和文档工具使得编写测试和生成文档变得简单而强大。通过遵循本章介绍的最佳实践,你可以确保你的 Rust 代码既可靠又易于使用。在下一章中,我们将探索 Rust 生态系统,了解常用的库和框架。

相关推荐
海外住宅ip供应商-luck2 分钟前
PHP如何搭建设置代理http并加密使用?
开发语言·http·php
Asthenia04124 分钟前
面试复盘:Collections.synchronizedList的实现与同步策略分析
后端
Source.Liu7 分钟前
【学Rust写CAD】18 定点数2D仿射变换矩阵结构体(MatrixFixedPoint结构别名)
矩阵·rust·cad
Asthenia041214 分钟前
面试复盘:CopyOnWriteArrayList的底层实现分析
后端
南玖yy15 分钟前
Python 的未来:在多元变革中持续领跑
开发语言·python
南隅。23 分钟前
【C++】基础3——正则表达式,静态多态(函数重载和模板),异常处理
开发语言·c++·正则表达式
Asthenia041224 分钟前
面试官问我:HashMap的扩容机制,我从jdk1.7和1.8两个版本来介绍
后端
计算机学姐26 分钟前
基于SpringBoot的足球俱乐部管理系统
java·vue.js·spring boot·后端·mysql·java-ee·intellij-idea
乌拉_乌拉_乌拉26 分钟前
C# string字符串介绍
开发语言·c#