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

ops/2025/3/31 0:47:11/

测试与文档

编写测试和文档是开发高质量软件的关键部分。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() {// 断言表达式为 trueassert!(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 命令提供了多种选项来控制测试执行:

# 运行所有测试
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` 是一个实用工具集合

运行文档测试

# 运行所有测试,包括文档测试
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 文档。

生成和查看文档

# 生成文档
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 依赖:

# 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);

运行基准测试:

cargo bench

属性测试

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

使用 proptest

添加 proptest 依赖:

# 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 工具测量代码覆盖率:

# 安装 tarpaulin
cargo install cargo-tarpaulin# 运行覆盖率分析
cargo tarpaulin

3. 模拟和测试替身

使用 mockall 库创建模拟对象:

# 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 生态系统,了解常用的库和框架。


http://www.ppmy.cn/ops/170524.html

相关文章

合宙780E开发学习-Lua语法速查

打印 print("测试") print("aabcdefg") print("xxxxx","第二个参数","第三个参数")代码注释 print("这段代码会运行") --print("我被注释掉了&#xff0c;所以不会运行") --[[我是多行注释不管我写多…

scala简介和基础语法

Scala简介 Scala 是一门多范式&#xff08;multi-paradigm&#xff09;的编程语言&#xff0c;设计初衷是要集成面向对象编程和函数式编程的各种特性。 Scala 运行在 Java 虚拟机上&#xff0c;并兼容现有的 Java 程序。Scala 源代码被编译成 Java 字节码&#xff0c;所以它可…

HTML5 MathML 学习笔记

一、什么是MathML MathML&#xff08;Mathematical Markup Language&#xff09;是一种数学标记语言&#xff0c;用于在互联网上书写数学符号和公式。MathML是一种基于XML的标准&#xff0c;可以用来描述复杂的数学公式和符号&#xff0c;使其能够在网页上正确显示。 MathML的…

光流 | 基于HS光流算法的稠密光流提取原理、分析、公式,改进措施,matlab完整代码

===================================================== github:https://github.com/MichaelBeechan CSDN:https://blog.csdn.net/u011344545 ===================================================== 稠密光流提取 一、HS光流算法原理与分析二、改进措施三、Matlab完整代码…

如何理解FFMPEG两个宏 1.MATCH_PER_TYPE_OPT, 2.MATCH_PER_STREAM_OPT

author: hjjdebug date: 2025年 03月 24日 星期一 14:47:15 CST description: 如何理解FFMPEG两个宏 1.MATCH_PER_TYPE_OPT, 2.MATCH_PER_STREAM_OPT 文章目录 1.MATCH_PER_TYPE_OPT1.1 宏定义1.2 宏调用1.3 宏展开1.4 宏展开详细解释1.5 测试代码:1.6 执行结果: 2. MATCH_PER_…

安当KSP密钥管理系统:量子安全时代的CA证书体系重构

在量子计算与AI大模型技术高速发展的今天&#xff0c;传统数字证书体系正面临**“算法脆弱性加剧”与“身份管理粗放化”的双重威胁。据NIST预测&#xff0c;2025年后量子计算机可在4小时内破解RSA-2048算法&#xff0c;而全球83%的CA系统仍依赖传统加密技术。上海安当推出的KS…

从简单场景认识建造者模式

建造者设计模式总的来说常见的形式无非就两种。 一种是具体产物样式多&#xff0c;故通过中间者&#xff08;指挥者&#xff09;来统筹决定产生哪种对象&#xff08;组装电脑&#xff0c;都是电脑&#xff0c;只是参数配置不同&#xff09;。 一种是构造的可选参数多&#xf…

OpenHarmony NativeC++应用开发speexdsp噪声消除案例

随着5.0的版本的迭代升级&#xff0c;笔者感受到了开源鸿蒙前所未有大的版本更替速度。5.0出现了越来越多的C API可以调用&#xff0c;极大的方便了native c应用的开发。笔者先将speexdsp噪声消除的案例分享&#xff0c;老规矩&#xff0c;还是开源&#xff01;&#xff01;&am…