
让代码靠谱的艺术:从单元测试到集成测试
你有没有过这样的经历:
要是有自动测试,你就能在上线前发现问题。今天咱们就聊聊 Rust 的测试系统——内置的、强大的、让你爱上写测试的神器。
类型 | 位置 | 用途 |
|---|---|---|
单元测试 | 源码文件内(#[cfg(test)]) | 测试单个函数/模块 |
集成测试 | tests/ 目录 | 测试整个库的 API |
文档测试 | 文档注释中(///) | 测试文档示例 |
生活化类比:
#[test]// 标记为测试函数
#[should_panic]// 期望 panic
#[ignore]// 跳过测试
#[test]
#[should_panic(expected = "错误信息")]// 期望特定 panic
cargo test# 运行所有测试
cargo test --lib # 只运行库测试
cargo test --test name # 运行指定集成测试
cargo test test_name # 运行匹配的测试
cargo test -- --nocapture # 显示 println 输出
cargo test -- --ignored # 运行被忽略的测试
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*; // 导入父模块的内容
#[test]
fn test_add() {
assert_eq!(add(, ), );
assert_eq!(add(-, ), );
assert_eq!(add(, ), );
}
}
说人话:
#[cfg(test)]:只在测试时编译这个模块use super::*:导入父模块(被测试的代码)#[test]:标记为测试函数#[test]
fn test_assertions() {
// assert!:必须为 true
assert!(true);
assert!( + == );
// assert_eq!:必须相等
assert_eq!(, + );
assert_eq!("hello", "hello");
// assert_ne!:必须不相等
assert_ne!(, );
// 带错误信息
assert!( + == , "数学出问题了!");
assert_eq!( + , , "2+2 应该等于 5,但现在不等于");
}
#[test]
fn test_result() {
let result: Result<i32, &str> = Ok();
assert!(result.is_ok());
let value = result.unwrap();
assert_eq!(value, );
}
#[test]
fn test_option() {
let opt: Option<i32> = Some();
assert!(opt.is_some());
assert_eq!(opt.unwrap(), );
let none: Option<i32> = None;
assert!(none.is_none());
}
pub fn divide(a: i32, b: i32) -> i32 {
if b == {
panic!("除数不能为零!");
}
a / b
}
#[test]
#[should_panic(expected = "除数不能为零")]
fn test_divide_by_zero() {
divide(, );
}
说人话:
#[should_panic] 表示这个测试应该 panic,如果没 panic 反而测试失败。
mod calculator {
// 私有函数
fn internal_add(a: i32, b: i32) -> i32 {
a + b
}
pub fn calculate(a: i32, b: i32) -> i32 {
internal_add(a, b)
}
#[cfg(test)]
mod tests {
use super::*; // 可以访问私有函数!
#[test]
fn test_internal() {
assert_eq!(internal_add(, ), ); // ✅ 可以测试私有函数
}
}
}
重点: 测试模块在父模块内部,可以访问私有成员。
my_project/
├── src/
│ └── lib.rs
├── tests/
│ ├── integration_test.rs
│ └── another_test.rs
└── Cargo.toml
tests/integration_test.rs:
use my_project; // 导入整个库
#[test]
fn test_public_api() {
// 只能测试 public API
let result = my_project::public_function();
assert_eq!(result, "expected");
}
注意:
tests/ 目录/// 计算两个数的和
///
/// # 示例
///
/// ```
/// let result = my_lib::add(2, 3);
/// assert_eq!(result, 5);
/// ```
///
/// # 错误
///
/// 如果溢出会 panic
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
运行文档测试:
cargo test --doc
说人话:
文档注释里的代码块会被自动测试!确保文档示例永远正确。
#[test]
fn test_add_multiple_cases() {
let test_cases = vec![
(, , ),
(, , ),
(-, , ),
(, , ),
];
for (a, b, expected) in test_cases {
assert_eq!(add(a, b), expected, "add({}, {}) 应该等于 {}", a, b, expected);
}
}
#[tokio::test]
async fn test_async_function() {
let result = async_function().await;
assert_eq!(result, "expected");
}
注意: 需要 tokio 的 macros 和 rt-multi-thread 特性。
#[test]
fn test_first() {
// 修改了全局状态
GLOBAL_STATE = ;
}
#[test]
fn test_second() {
// 依赖第一个测试的状态
assert_eq!(GLOBAL_STATE, ); // ❌ 不可靠!
}
问题: 测试不应该依赖执行顺序(顺序可能变化)。
解决方案:
#[test]
fn test_first() {
let mut state = ; // 局部状态
assert_eq!(state, );
}
#[test]
fn test_second() {
let mut state = ; // 独立的局部状态
assert_eq!(state, );
}
#[test]
fn test_slow() {
// 耗时 5 秒
std::thread::sleep(std::time::Duration::from_secs());
}
解决方案:
#[test]
#[ignore]// 默认跳过
fn test_slow() {
std::thread::sleep(std::time::Duration::from_secs());
}
运行忽略的测试:
cargo test -- --ignored
#[test]
fn test_debug() {
println!("调试信息"); // ❌ 默认不显示
}
解决方案:
cargo test -- --nocapture
// tests/integration_test.rs
use my_project::internal_module; // ❌ 编译错误
问题: 集成测试只能访问 public API。
解决方案:
// src/lib.rs
pub mod internal_module { // 改成 public
// ...
}
或者通过 public API 测试。
#[test]
fn test_with_env() {
let api_key = std::env::var("API_KEY").unwrap(); // ❌ 可能没有
}
解决方案:
#[test]
fn test_with_env() {
let api_key = std::env::var("API_KEY")
.unwrap_or_else(|_| "test_key".to_string());
}
或者用 .env 文件:
# 安装 dotenvy
cargo add dotenvy
# 创建 .env 文件
API_KEY=test_key
// 测试前加载
dotenvy::dotenv().ok();
// src/lib.rs
pub struct Calculator {
value: i32,
}
impl Calculator {
pub fn new() -> Self {
Calculator { value: }
}
pub fn add(&mut self, n: i32) {
self.value += n;
}
pub fn subtract(&mut self, n: i32) {
self.value -= n;
}
pub fn get_value(&self) -> i32 {
self.value
}
pub fn reset(&mut self) {
self.value = ;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let calc = Calculator::new();
assert_eq!(calc.get_value(), );
}
#[test]
fn test_add() {
let mut calc = Calculator::new();
calc.add();
assert_eq!(calc.get_value(), );
}
#[test]
fn test_chain() {
let mut calc = Calculator::new();
calc.add();
calc.subtract();
calc.add();
assert_eq!(calc.get_value(), );
}
#[test]
fn test_reset() {
let mut calc = Calculator::new();
calc.add();
calc.reset();
assert_eq!(calc.get_value(), );
}
}
// src/lib.rs
#[derive(Debug, PartialEq)]
pub enum MathError {
DivisionByZero,
Overflow,
}
pub fn safe_divide(a: i32, b: i32) -> Result<i32, MathError> {
if b == {
return Err(MathError::DivisionByZero);
}
a.checked_div(b).ok_or(MathError::Overflow)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_divide_ok() {
assert_eq!(safe_divide(, ), Ok());
}
#[test]
fn test_safe_divide_by_zero() {
assert_eq!(safe_divide(, ), Err(MathError::DivisionByZero));
}
#[test]
fn test_safe_divide_overflow() {
assert_eq!(safe_divide(i32::MIN, -), Err(MathError::Overflow));
}
}
// src/lib.rs
pub trait Drawable {
fn draw(&self) -> String;
}
pub struct Circle {
pub radius: f64,
}
impl Drawable for Circle {
fn draw(&self) -> String {
format!("Circle with radius {}", self.radius)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circle_draw() {
let circle = Circle { radius: 5.0 };
assert_eq!(circle.draw(), "Circle with radius 5");
}
}
// Cargo.toml
[dev-dependencies]
tokio = { version = "1.0", features = ["full"] }
// src/lib.rs
pub async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
reqwest::get(url).await?.text().await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_fetch_data() {
// 用 mock server 测试
let result = fetch_data("https://example.com").await;
assert!(result.is_ok());
}
}

金句回顾:
#[cfg(test)]:测试代码只在测试时编译assert!、assert_eq!、assert_ne! 三剑客下篇预告:
咱们已经学了内存的基础(栈和堆),但 Rust 的内存模型可不止这些。下篇深入聊聊Rust 的内存模型,看看所有权、借用、生命周期是怎么在内存层面工作的!