首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >20-Rust 教程 - 测试

20-Rust 教程 - 测试

作者头像
LarryLan
发布2026-05-14 18:13:35
发布2026-05-14 18:13:35
800
举报

测试

让代码靠谱的艺术:从单元测试到集成测试

🎬 引入

你有没有过这样的经历:

  1. 改了一行代码
  2. 觉得自己很牛逼
  3. 直接上线
  4. 第二天用户反馈:全崩了

要是有自动测试,你就能在上线前发现问题。今天咱们就聊聊 Rust 的测试系统——内置的、强大的、让你爱上写测试的神器。

📌 核心概念

Rust 的测试类型

类型

位置

用途

单元测试

源码文件内(#[cfg(test)])

测试单个函数/模块

集成测试

tests/ 目录

测试整个库的 API

文档测试

文档注释中(///)

测试文档示例

生活化类比:

  • 单元测试:检查每个零件(螺丝、齿轮、电路板)
  • 集成测试:组装后检查整机功能
  • 文档测试:说明书上的示例能不能照做

测试的核心属性

代码语言:javascript
复制
#[test]// 标记为测试函数
#[should_panic]// 期望 panic
#[ignore]// 跳过测试
#[test]
#[should_panic(expected = "错误信息")]// 期望特定 panic

Cargo 测试命令

代码语言:javascript
复制
cargo test# 运行所有测试
cargo test --lib        # 只运行库测试
cargo test --test name  # 运行指定集成测试
cargo test test_name    # 运行匹配的测试
cargo test -- --nocapture  # 显示 println 输出
cargo test -- --ignored    # 运行被忽略的测试

💻 代码示例

基础示例:单元测试

代码语言:javascript
复制
// 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]:标记为测试函数

断言宏

代码语言:javascript
复制
#[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,但现在不等于");
}

测试 Result 和 Option

代码语言:javascript
复制
#[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());
}

测试 panic

代码语言:javascript
复制
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 反而测试失败。

测试私有函数

代码语言:javascript
复制
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(, ), );  // ✅ 可以测试私有函数
        }
    }
}

重点: 测试模块在父模块内部,可以访问私有成员。

集成测试

代码语言:javascript
复制
my_project/
├── src/
│   └── lib.rs
├── tests/
│   ├── integration_test.rs
│   └── another_test.rs
└── Cargo.toml

tests/integration_test.rs

代码语言:javascript
复制
use my_project;  // 导入整个库

#[test]
fn test_public_api() {
    // 只能测试 public API
    let result = my_project::public_function();
    assert_eq!(result, "expected");
}

注意:

  • 集成测试在 tests/ 目录
  • 每个文件是独立的 crate
  • 只能测试 public API

文档测试

代码语言:javascript
复制
/// 计算两个数的和
///
/// # 示例
///
/// ```
/// let result = my_lib::add(2, 3);
/// assert_eq!(result, 5);
/// ```
///
/// # 错误
///
/// 如果溢出会 panic
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

运行文档测试:

代码语言:javascript
复制
cargo test --doc

说人话:

文档注释里的代码块会被自动测试!确保文档示例永远正确。

参数化测试

代码语言:javascript
复制
#[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);
    }
}

测试异步代码

代码语言:javascript
复制
#[tokio::test]
async fn test_async_function() {
    let result = async_function().await;
    assert_eq!(result, "expected");
}

注意: 需要 tokiomacrosrt-multi-thread 特性。

🐛 常见坑点

坑点 1:测试顺序依赖

代码语言:javascript
复制
#[test]
fn test_first() {
    // 修改了全局状态
    GLOBAL_STATE = ;
}

#[test]
fn test_second() {
    // 依赖第一个测试的状态
    assert_eq!(GLOBAL_STATE, );  // ❌ 不可靠!
}

问题: 测试不应该依赖执行顺序(顺序可能变化)。

解决方案:

代码语言:javascript
复制
#[test]
fn test_first() {
    let mut state = ;  // 局部状态
    assert_eq!(state, );
}

#[test]
fn test_second() {
    let mut state = ;  // 独立的局部状态
    assert_eq!(state, );
}

坑点 2:测试耗时太长

代码语言:javascript
复制
#[test]
fn test_slow() {
    // 耗时 5 秒
    std::thread::sleep(std::time::Duration::from_secs());
}

解决方案:

代码语言:javascript
复制
#[test]
#[ignore]// 默认跳过
fn test_slow() {
    std::thread::sleep(std::time::Duration::from_secs());
}

运行忽略的测试:

代码语言:javascript
复制
cargo test -- --ignored

坑点 3:测试输出看不到

代码语言:javascript
复制
#[test]
fn test_debug() {
    println!("调试信息");  // ❌ 默认不显示
}

解决方案:

代码语言:javascript
复制
cargo test -- --nocapture

坑点 4:集成测试找不到模块

代码语言:javascript
复制
// tests/integration_test.rs
use my_project::internal_module;  // ❌ 编译错误

问题: 集成测试只能访问 public API。

解决方案:

代码语言:javascript
复制
// src/lib.rs
pub mod internal_module {  // 改成 public
    // ...
}

或者通过 public API 测试。

坑点 5:测试环境配置

代码语言:javascript
复制
#[test]
fn test_with_env() {
    let api_key = std::env::var("API_KEY").unwrap();  // ❌ 可能没有
}

解决方案:

代码语言:javascript
复制
#[test]
fn test_with_env() {
    let api_key = std::env::var("API_KEY")
        .unwrap_or_else(|_| "test_key".to_string());
}

或者用 .env 文件:

代码语言:javascript
复制
# 安装 dotenvy
cargo add dotenvy

# 创建 .env 文件
API_KEY=test_key
代码语言:javascript
复制
// 测试前加载
dotenvy::dotenv().ok();

🎯 实战案例

案例 1:完整测试示例

代码语言:javascript
复制
// 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(), );
    }
}

案例 2:错误处理测试

代码语言:javascript
复制
// 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));
    }
}

案例 3:测试 trait 实现

代码语言:javascript
复制
// 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");
    }
}

案例 4:测试异步代码

代码语言:javascript
复制
// 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());
    }
}

🧠 思维导图

20-测试
20-测试

📝 小结

金句回顾:

  1. 三种测试:单元、集成、文档,缺一不可
  2. #[cfg(test)]:测试代码只在测试时编译
  3. 断言宏assert!assert_eq!assert_ne! 三剑客
  4. 测试要独立:不依赖顺序,不共享状态
  5. 文档测试:让文档示例永远正确

下篇预告:

咱们已经学了内存的基础(栈和堆),但 Rust 的内存模型可不止这些。下篇深入聊聊Rust 的内存模型,看看所有权、借用、生命周期是怎么在内存层面工作的!

🔗 参考资料

  • Testing Chapter
  • Integration Tests
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Larry的Hub 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 测试
    • 🎬 引入
    • 📌 核心概念
      • Rust 的测试类型
      • 测试的核心属性
      • Cargo 测试命令
    • 💻 代码示例
      • 基础示例:单元测试
      • 断言宏
      • 测试 Result 和 Option
      • 测试 panic
      • 测试私有函数
      • 集成测试
      • 文档测试
      • 参数化测试
      • 测试异步代码
    • 🐛 常见坑点
      • 坑点 1:测试顺序依赖
      • 坑点 2:测试耗时太长
      • 坑点 3:测试输出看不到
      • 坑点 4:集成测试找不到模块
      • 坑点 5:测试环境配置
    • 🎯 实战案例
      • 案例 1:完整测试示例
      • 案例 2:错误处理测试
      • 案例 3:测试 trait 实现
      • 案例 4:测试异步代码
    • 🧠 思维导图
    • 📝 小结
    • 🔗 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档