第十八章:测试与文档——质量的守护者
本章导读:代码不仅要能运行,还要能正确运行。测试是保证代码质量的护城河,文档是帮助他人(和未来的自己)理解代码的灯塔。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)] |
| 集成测试 | 测试公共 API | tests/ 目录 |
| 文档测试 | 示例即测试 | 文档注释中 |
| 文档元素 | 语法 |
|---|---|
| 模块文档 | //! |
| 项文档 | /// |
| 代码块 | ` |
| 链接 | [Item] |
关键原则:
- 测试要快、独立、可重复
- 文档要有示例、说明边界条件
- 保持文档与代码同步
动手实验:
- 为之前的
Calculator添加pow(幂运算)方法,并编写测试。- 创建一个
tests/目录,编写集成测试。- 为你的库添加完整的文档注释,包括 Examples、Panics、Errors 等。
- 使用 criterion 为
fibonacci函数编写基准测试。