测试
测试是确保代码正确性的重要手段。Rust 内置了测试框架,无需额外配置即可编写和运行测试。
测试概述
Rust 的测试框架提供了编写单元测试和集成测试的能力:
┌─────────────────────────────────────────────────────────────┐
│ Rust 测试类型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 单元测试 (Unit Tests) 集成测试 (Integration Tests) │
│ ───────────────────── ──────────────────────────── │
│ 测试单个函数或模块 测试多个模块的协作 │
│ 与代码放在同一文件 放在 tests 目录 │
│ 可以测试私有函数 只能测试公开 API │
│ 使用 #[cfg(test)] 独立的测试模块 │
│ │
│ 文档测试 (Doc Tests) │
│ ───────────────────── │
│ 在文档注释中编写示例 │
│ 自动验证代码示例的正确性 │
│ │
└─────────────────────────────────────────────────────────────┘
编写测试
基本测试
使用 #[test] 属性标记测试函数:
#[test]
fn it_works() {
assert!(true);
}
#[test]
fn another_test() {
assert_eq!(2 + 2, 4);
}
运行测试:
cargo test
断言宏
Rust 提供了多种断言宏用于验证测试结果:
#[test]
fn test_assertions() {
// assert! - 验证表达式为 true
assert!(true);
assert!(5 > 3, "5 应该大于 3");
// assert_eq! - 验证两个值相等
assert_eq!(2 + 2, 4);
assert_eq!("hello".to_uppercase(), "HELLO");
// assert_ne! - 验证两个值不相等
assert_ne!(1, 2);
assert_ne!("hello", "world");
}
#[test]
fn test_with_custom_message() {
let result = 2 + 2;
assert_eq!(
result,
4,
"计算结果应该是 4,但实际是 {}",
result
);
}
测试失败
测试失败有两种方式:断言失败和 panic:
#[test]
fn test_failure() {
// 断言失败
assert!(false, "这个测试会失败");
}
#[test]
fn test_panic() {
// panic 导致测试失败
panic!("这个测试会崩溃");
}
测试应该 panic
使用 #[should_panic] 验证代码是否按预期 panic:
#[test]
#[should_panic]
fn test_panic_expected() {
panic!("这个 panic 是预期的");
}
#[test]
#[should_panic(expected = "除数不能为零")]
fn test_divide_by_zero() {
divide(10, 0);
}
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("除数不能为零");
}
a / b
}
使用 Result 的测试
测试函数可以返回 Result<T, E>:
#[test]
fn test_result() -> Result<(), String> {
let result = 2 + 2;
if result == 4 {
Ok(())
} else {
Err(String::from("结果不是 4"))
}
}
#[test]
fn test_parse() -> Result<(), std::num::ParseIntError> {
let number: i32 = "42".parse()?;
assert_eq!(number, 42);
Ok(())
}
单元测试
单元测试与被测试代码放在同一文件中,使用 #[cfg(test)] 模块:
测试模块结构
// src/lib.rs 或 src/main.rs
// 被测试的函数
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
fn internal_function() -> i32 {
42
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_internal() {
// 可以测试私有函数
assert_eq!(internal_function(), 42);
}
}
解释:
#[cfg(test)]:只在运行cargo test时编译此模块use super::*:导入父模块的所有内容- 测试私有函数:单元测试可以访问私有函数
测试示例
pub fn is_even(n: i32) -> bool {
n % 2 == 0
}
pub fn factorial(n: u32) -> u32 {
if n == 0 {
1
} else {
n * factorial(n - 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_even() {
assert!(is_even(2));
assert!(is_even(0));
assert!(!is_even(1));
assert!(!is_even(3));
}
#[test]
fn test_factorial() {
assert_eq!(factorial(0), 1);
assert_eq!(factorial(1), 1);
assert_eq!(factorial(5), 120);
}
#[test]
fn test_factorial_edge_cases() {
// 测试边界情况
assert_eq!(factorial(0), 1);
assert_eq!(factorial(1), 1);
}
}
集成测试
集成测试放在 tests 目录下,只能测试公开 API。
创建集成测试
my_project/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
└── integration_test.rs
// tests/integration_test.rs
use my_project::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_public_api() {
// 只能测试公开函数
assert!(is_even(4));
}
共享测试模块
在 tests 目录下创建公共模块:
tests/
├── common/
│ └── mod.rs
└── integration_test.rs
// tests/common/mod.rs
pub fn setup() {
// 测试前的初始化工作
}
pub fn create_test_data() -> Vec<i32> {
vec![1, 2, 3, 4, 5]
}
// tests/integration_test.rs
mod common;
use my_project::*;
#[test]
fn test_with_setup() {
common::setup();
let data = common::create_test_data();
assert_eq!(data.len(), 5);
}
文档测试
文档注释中的代码示例会被自动测试:
/// 将两个数相加
///
/// # Examples
///
/// ```
/// use my_project::add;
///
/// let result = add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// 计算阶乘
///
/// # Examples
///
/// ```
/// use my_project::factorial;
///
/// assert_eq!(factorial(0), 1);
/// assert_eq!(factorial(5), 120);
/// ```
///
/// # Panics
///
/// 输入过大时会 panic
pub fn factorial(n: u32) -> u32 {
if n == 0 {
1
} else {
n * factorial(n - 1)
}
}
忽略文档测试
/// 示例代码(不运行测试)
///
/// ```ignore
/// use my_project::some_function;
///
/// // 这个代码块不会被执行
/// some_function();
/// ```
pub fn some_function() {}
/// 编译但不运行
///
/// ```no_run
/// use my_project::long_running;
///
/// // 编译通过但不运行
/// long_running();
/// ```
pub fn long_running() {
loop {}
}
/// 允许编译错误
///
/// ```compile_fail
/// let x: i32 = "hello"; // 编译错误
/// ```
pub fn compile_fail_example() {}
运行测试
基本命令
# 运行所有测试
cargo test
# 运行特定测试
cargo test test_name
# 运行名称匹配的测试
cargo test keyword
# 运行单元测试
cargo test --lib
# 运行集成测试
cargo test --test integration_test
# 运行文档测试
cargo test --doc
控制测试行为
# 显示测试输出
cargo test -- --nocapture
# 并行运行测试(默认)
cargo test
# 串行运行测试
cargo test -- --test-threads=1
# 只运行被忽略的测试
cargo test -- --ignored
# 运行所有测试(包括被忽略的)
cargo test -- --include-ignored
忽略测试
#[test]
#[ignore]
fn expensive_test() {
// 耗时的测试
}
#[test]
#[ignore = "需要外部依赖"]
fn requires_external_service() {
// 需要外部服务的测试
}
测试组织
测试辅助函数
#[cfg(test)]
mod tests {
use super::*;
// 辅助函数
fn create_test_user() -> User {
User {
name: String::from("test"),
age: 25,
}
}
#[test]
fn test_user_name() {
let user = create_test_user();
assert_eq!(user.name, "test");
}
#[test]
fn test_user_age() {
let user = create_test_user();
assert_eq!(user.age, 25);
}
}
struct User {
name: String,
age: u32,
}
测试夹具(Fixture)
#[cfg(test)]
mod tests {
use super::*;
struct TestContext {
data: Vec<i32>,
}
impl TestContext {
fn new() -> Self {
TestContext {
data: vec![1, 2, 3, 4, 5],
}
}
}
#[test]
fn test_with_context() {
let ctx = TestContext::new();
assert_eq!(ctx.data.len(), 5);
}
}
测试最佳实践
1. 测试命名
#[cfg(test)]
mod tests {
use super::*;
// 好的命名:描述测试行为
#[test]
fn add_returns_sum_of_two_numbers() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn is_even_returns_false_for_odd_numbers() {
assert!(!is_even(3));
}
#[test]
fn factorial_returns_1_for_zero() {
assert_eq!(factorial(0), 1);
}
}
2. 测试边界情况
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normal_case() {
assert_eq!(divide(10, 2), 5);
}
#[test]
fn test_edge_cases() {
// 边界情况
assert_eq!(divide(0, 1), 0); // 零作为被除数
assert_eq!(divide(1, 1), 1); // 相同的数
}
#[test]
#[should_panic(expected = "除数不能为零")]
fn test_error_case() {
divide(10, 0); // 错误情况
}
}
3. 一个测试一个概念
#[cfg(test)]
mod tests {
use super::*;
// 好:每个测试只验证一个概念
#[test]
fn is_even_returns_true_for_even_numbers() {
assert!(is_even(2));
assert!(is_even(4));
assert!(is_even(0));
}
#[test]
fn is_even_returns_false_for_odd_numbers() {
assert!(!is_even(1));
assert!(!is_even(3));
}
}
4. 使用有意义的断言消息
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_with_meaningful_messages() {
let result = add(2, 3);
assert_eq!(
result,
5,
"add(2, 3) 应该返回 5,但实际返回了 {}",
result
);
}
}
5. 测试错误处理
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_handling() -> Result<(), String> {
let result = parse_number("42")?;
assert_eq!(result, 42);
Ok(())
}
#[test]
fn test_invalid_input() {
let result = parse_number("not a number");
assert!(result.is_err());
}
}
fn parse_number(s: &str) -> Result<i32, String> {
s.parse().map_err(|_| format!("无法解析 '{}' 为数字", s))
}
测试覆盖率
使用 cargo-tarpaulin
# 安装
cargo install cargo-tarpaulin
# 运行覆盖率测试
cargo tarpaulin
使用 cargo-llvm-cov
# 安装
cargo install cargo-llvm-cov
# 运行覆盖率测试
cargo llvm-cov
小结
本章我们学习了:
- 测试基础:使用
#[test]编写测试 - 断言宏:
assert!、assert_eq!、assert_ne! - 单元测试:与代码放在同一文件
- 集成测试:放在
tests目录 - 文档测试:自动测试文档示例
- 测试组织:命名、边界情况、最佳实践
练习
- 为一个计算器模块编写完整的单元测试
- 编写集成测试测试多个模块的协作
- 为一个函数编写文档测试
- 测试一个可能失败的函数的错误处理