手把手带你写 Rust 测试

手把手带你写 Rust 测试

Rust 从语言层面内置了一套开箱即用、零第三方依赖的测试框架,原生支持单元测试、集成测试、文档测试三大核心场景,实现了开发即测试、文档即测试的理念。

第一个单元测试用例

我们从最简单的单元测试场景入手,先写一个业务函数,再为它编写对应的测试用例。

打开 src/lib.rs,写入两个基础的业务函数:

rust 复制代码
/// 两个整数相加
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// 两个整数相除
pub fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Division by zero is not allowed");
    }
    a / b
}

在 src/lib.rs 文件末尾,添加 Rust 约定的单元测试模块:

rust 复制代码
// 测试函数通过 #[test] 属性标记,只有标记了该属性的函数才会被当作测试用例执行
#[cfg(test)]
mod tests {
    // 引入父模块的所有内容,可直接访问业务函数
    use super::*;

    // 标记该函数为测试用例
    #[test]
    fn add_should_return_correct_result() {
        // 执行业务逻辑
        let result = add(2, 2);
        // 断言:验证结果是否符合预期
        assert_eq!(result, 4);
    }
}

在项目根目录执行以下命令,运行所有测试:

shell 复制代码
cargo test

你会看到如下输出,代表测试执行成功:

plaintext 复制代码
running 1 test
test tests::add_should_return_correct_result ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Rust 内置了三个最常用的断言宏,如下所示:

宏 作用 示例
assert! 验证布尔表达式为 true assert!(add(2, 2) == 4);
assert_eq! 验证两个值相等 assert_eq!(add(2, 2), 4);
assert_ne! 验证两个值不相等 assert_ne!(add(2, 2), 5);

所有断言宏都支持自定义错误信息,方便调试定位问题:

rust 复制代码
#[test]
fn add_with_custom_error_msg() {
    let result = add(3, 5);
    assert_eq!(result, 8, "3 + 5 should be 8, but got {}", result);
}

错误处理场景

在实际开发中,除了正常逻辑,我们还需要测试不可恢复错误、Result 错误等边界场景,Rust 默认提供了相对应的方案。

测试 Panic 场景

当函数在特定场景下会触发 panic 时(比如上面的除零操作),我们可以通过 #[should_panic] 属性标记测试用例,验证 panic 是否会如期发生。

rust 复制代码
#[test]
// 标记该测试用例预期会触发 panic
#[should_panic]
fn divide_by_zero_should_panic() {
    divide(10, 0);
}

执行 cargo test,该测试会顺利通过。但这里有个问题:如果被测试的函数可能会触发多种 panic,我们如何去测试特定的 panic 呢?

为了精准匹配 panic 场景,我们可以添加 expected 参数,指定预期的 panic 信息:

rust 复制代码
#[test]
#[should_panic(expected = "Division by zero is not allowed")]
fn divide_by_zero_should_panic_with_exact_msg() {
    divide(10, 0);
}

只有当 panic 信息包含指定的字符串时,测试才会通过,避免误判。

测试 Result 场景

除了 panic,Rust 中更常用的错误处理方式是 Result<T, E> 类型。Rust 测试原生支持返回 Result 类型,无需 panic,还能使用 ? 运算符简化错误传播,代码更简洁优雅。

首先添加一个新函数 divide_safe,返回 Result 类型:

rust 复制代码
/// 安全的整数除法,返回 Result 类型
pub fn divide_safe(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        return Err("Division by zero is not allowed".to_string());
    }
    Ok(a / b)
}

然后编写对应的测试用例:

rust 复制代码
#[test]
fn divide_safe_should_return_correct_result() -> Result<(), String> {
    // 使用 ? 运算符,出错时直接返回 Err,终止测试
    assert_eq!(divide_safe(10, 2)?, 5);
    // 验证错误场景
    assert!(divide_safe(10, 0).is_err());
    // 测试通过,返回 Ok(())
    Ok(())
}

这种方式更符合 Rust 的错误处理哲学,也是官方推荐的错误场景测试方案。

第一个集成测试用例

集成测试用于验证库的对外公有 API 的行为是否符合预期,测试用例统一放在项目根目录的 tests 文件夹下,与 src 同级。

在项目根目录创建 tests 文件夹,再创建 tests/integration_test.rs,写入以下代码:

rust 复制代码
use test_example::*;

#[test]
fn test_add_public_api() {
    // 测试公有API的行为
    assert_eq!(add(10, 20), 30);
    assert_eq!(add(-5, 5), 0);
}

#[test]
fn test_divide_safe_public_api() {
    assert!(divide_safe(100, 10).is_ok());
    assert!(divide_safe(100, 0).is_err());
}

执行 cargo test,你会看到输出中新增了集成测试的执行结果,同时单元测试也会一起执行。如果只想执行指定的集成测试文件,可以使用以下命令:

shell 复制代码
# 仅执行 integration_test.rs 中的测试
cargo test --test integration_test

在实际开发中,多个集成测试文件经常需要复用一些通用逻辑(比如环境初始化、数据准备等),而且我们还不想把它们写到业务目录 src 中。

这时候,我们可以创建 tests/common/mod.rs,当然创建为 tests/common.rs 也可行,但不推荐,因为 Cargo 会将该文件视为测试 crate 并尝试运行其中的测试。

示例:创建 tests/common/mod.rs,写入通用逻辑:

rust 复制代码
pub fn setup_test_env() {
    println!("初始化测试环境:加载配置、连接数据库等");
    // 通用的测试初始化逻辑
}

在集成测试中引用通用模块:

rust 复制代码
// 引入通用模块
mod common;

#[test]
fn test_with_env_setup() {
    // 调用共享的初始化函数
    common::setup_test_env();
    assert_eq!(add(5, 5), 10);
}

第一个文档测试用例

Rust 最具特色的测试功能就是原生支持文档即测试,即文档注释中的代码块,会被 cargo test 自动识别为测试用例并执行,解决文档与代码不同步的痛点。

给 add 函数添加带测试的文档注释:

rust 复制代码
/// 两个整数相加
/// 
/// # Examples
/// 
/// ```
/// // 文档测试的代码块会被自动执行
/// use test_example::add;
/// 
/// assert_eq!(add(2, 3), 5);
/// assert_eq!(add(-1, 1), 0);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

执行 cargo test,你会看到输出中新增了 Doc-tests 部分,这说明文档测试已经被执行。

第三方库推荐

当内置的测试能力无法满足复杂场景时,可以使用社区成熟的第三方库扩展测试能力:

  • mockall,强大的 Mock 框架,模拟依赖接口,隔离测试环境
  • faker,假数据生成,支持多语言
  • criterion,Rust 生态系统中最流行的基准测试工具
  • cargo-fuzz,模糊测试工具,挖掘代码中的边界漏洞
  • cargo-nextest,新一代的 Rust 测试运行器,能够极大提升测试性能,可以完全替代 cargo test 命令。

总结

最后,好的测试不仅能保证代码的正确性,还能倒逼你写出更清晰、更易维护、耦合度更低的代码。希望本文能帮你快速掌握 Rust 测试的核心能力,写出更健壮的 Rust 项目。

相关推荐
小灰灰搞电子16 小时前
Rust+Slint 实现高级动态消息提示框源码分享
开发语言·rust
qq_4069810317 小时前
大白话rust系列08 当变量遮蔽遇上引用,探究变量遮蔽的本质
开发语言·后端·rust
轻舟技术A18 小时前
Pi、rpi、Pi-Bolt 重测:rpi 在启动、内存与读取上全面领先
人工智能·rust
用户144984334647519 小时前
我为什么做一个开源的 Mac 清理工具
rust
古城小栈21 小时前
Rust 学习路线,供初学者参考
开发语言·rust
小熊吃保安2 天前
Windows也能用 Apple Watch 解锁
windows·rust·开源
离歌漠3 天前
《我用 Tauri 做了一个本地开发工具箱》
rust·typescript·vue
小小龙学IT3 天前
Python Polars 高性能数据处理引擎深度解析
开发语言·python·rust·1024程序员节
zzu123zsw3 天前
为什么 Python 做 Agent 总在并发中崩溃?拆解 Blockcell 在 Rust 下的 Host-Cell 隔离与自愈状态机
开发语言·python·rust·大模型·ai agent