测试与文档
编写测试和文档是开发高质量软件的关键部分。Rust 提供了强大的测试框架和文档生成工具,使开发者能够确保代码的正确性并提供清晰的使用指南。在本章中,我们将探索 Rust 的测试系统和文档工具。
Rust 的测试哲学
Rust 的测试系统内置于语言和构建工具中,鼓励开发者将测试作为开发过程的一部分。Rust 支持多种测试类型:
- 单元测试:测试单个函数或模块的功能
- 集成测试:测试多个部分如何协同工作
- 文档测试:确保文档中的代码示例是正确的
单元测试
单元测试通常与被测试的代码放在同一个文件中,使用 #[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!
:断言一个布尔表达式为 trueassert_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)
测试驱动开发是一种先编写测试,然后实现功能的方法:
- 编写一个失败的测试
- 实现最小代码使测试通过
- 重构代码,保持测试通过
- 重复上述步骤
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. 测试覆盖率
使用 grcov
或 tarpaulin
工具测量代码覆盖率:
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)
}
练习题
-
为一个简单的计算器库编写单元测试,测试加、减、乘、除四种基本运算。确保除法函数在除数为零时正确处理错误。
-
创建一个字符串处理库,包含至少三个函数(如反转字符串、计算单词数、转换大小写等)。为每个函数编写文档注释,包括示例代码,并确保文档测试能够通过。
-
实现一个简单的栈数据结构,并为其编写集成测试。测试应该覆盖压栈、弹栈、查看栈顶元素和检查栈是否为空等操作。
-
使用属性测试(如 proptest)为一个排序函数编写测试,验证排序后的数组满足以下属性:元素已排序、元素数量不变、所有原始元素都存在。
-
为一个现有的 Rust 项目添加基准测试,比较至少两种不同实现的性能。使用 Criterion 库记录和可视化性能结果。
总结
在本章中,我们探讨了 Rust 的测试和文档系统:
- 单元测试、集成测试和文档测试的编写和运行
- 使用断言宏和
should_panic
属性测试代码行为 - 基准测试和属性测试的基础知识
- 编写清晰、全面的文档注释
- 生成和组织 API 文档
- 测试和文档的最佳实践
测试和文档是高质量软件开发的关键部分。Rust 的内置测试框架和文档工具使得编写测试和生成文档变得简单而强大。通过遵循本章介绍的最佳实践,你可以确保你的 Rust 代码既可靠又易于使用。在下一章中,我们将探索 Rust 生态系统,了解常用的库和框架。