讲解

Rust 的测试设施内建在语言和工具链里,不需要任何第三方框架。最小单元是测试函数:普通函数加上 #[test] 属性,cargo test 就会自动发现并执行它。测试通常和被测代码放在同一个文件里:文件底部加一个 #[cfg(test)] 标记的 mod tests 子模块——cfg(test) 表示「只在 cargo test 时编译」,正常构建完全不包含它,零体积负担。子模块里用 use super::* 引入上层代码,还能测试私有函数(单元测试的特权)。

断言三兄弟:assert!(条件)、assert_eq!(左, 右)(不等时打印两边的值,排错体验好)、assert_ne!(断言不等)。测试失败的方式就是 panic——所以验证「某段代码应当 panic」用 #[should_panic] 属性,加 expected = "..." 还能校验 panic 消息包含指定文本,防止「panic 了但不是你要的那个 panic」。

cargo test 的常用姿势:cargo test 跑全部;cargo test 关键词 按名字过滤(如 cargo test add 只跑名字含 add 的);测试默认并行执行且捕获输出,想看 println! 输出加 -- --nocapture;默认不跑标注了 #[ignore] 的慢测试,cargo test -- --ignored 专门跑它们。模块级测试之外,项目根的 tests/ 目录放集成测试(以库的外部用户身份调用公开 API),写法相同、发现机制相同。

示例

下面的 bash 块会真实创建一个库项目、写入带测试的代码并运行 cargo test(本教程构建时实际执行):

cargo new calc --lib
cat > calc/src/lib.rs <<'EOF'
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("除数不能为零");
    }
    a / b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_works() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn add_negative() {
        assert!(add(-1, -1) == -2);
        assert_ne!(add(-1, 1), 2);
    }

    #[test]
    fn divide_normal() {
        assert_eq!(divide(10, 2), 5);
    }

    #[test]
    #[should_panic(expected = "除数不能为零")]
    fn divide_by_zero_panics() {
        divide(1, 0);
    }
}
EOF
cd calc && cargo test

运行结果的末尾几行长这样(4 个测试全部通过):

running 4 tests
test tests::add_negative ... ok
test tests::add_works ... ok
test tests::divide_normal ... ok
test tests::divide_by_zero_panics - should panic ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

常见坑

  • assert_eq! 参数顺序没信息量:惯例是 assert_eq!(实际值, 期望值);失败输出按 left/right 标注,写反了读报错时会被绕晕。
  • 对浮点 assert_eq!:浮点误差会让 0.1 + 0.2 的断言挂掉;断言差值小于一个 epsilon(assert!((a - b).abs() < 1e-10))。
  • 测试之间共享可变状态:测试默认并行跑,共享文件、全局变量会造成偶发失败;每个测试自带独立数据。
  • 测试代码塞进主代码:忘了 #[cfg(test)] 会把测试编译进正式产物;约定就是文件底部 mod tests + cfg(test)。

小结

#[test] 标函数、#[cfg(test)] 包测试模块、assert 三兄弟做断言、#[should_panic] 验证崩溃、cargo test 一键运行——测试是 Rust 项目的标配而不是选配。至此 25 章全部完成:从安装、所有权三件套到 trait、生命周期、并发与测试,你已经具备阅读中小型 Rust 项目和动手写工具程序的全部基础。剩下的路在真实代码里:挑一个日常小需求,用 Rust 实现它。