跳至内容

测试

这篇讲 Rust 的测试体系——单元测试、集成测试和文档测试。Rust 把测试作为语言的一等公民内置,无需第三方测试框架。

单元测试

#[test] 属性标记测试函数,通常放在与被测试代码同一个文件中:

// src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
    left + right
}

#[cfg(test)]  // 只在 cargo test 时编译
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 2), 4);
    }

    #[test]
    fn test_add_zero() {
        assert_eq!(add(0, 5), 5);
    }
}

运行测试:

cargo test                  # 运行所有测试
cargo test test_add         # 按名称过滤
cargo test -- --nocapture   # 显示测试中的 println! 输出

断言宏

#[test]
fn assertion_demo() {
    assert!(true);                           // 布尔值为 true
    assert_eq!(2 + 2, 4);                   // 相等(需要 PartialEq + Debug)
    assert_ne!(2 + 2, 5);                   // 不等
    assert_eq!(
        vec![1, 2, 3],
        vec![1, 2, 3]
    );
}

#[test]
#[should_panic(expected = "除数不能为零")]   // 期望 panic
fn test_divide_by_zero() {
    divide(10, 0);
}

fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("除数不能为零");
    }
    a / b
}
assert_eq!assert_ne! 在失败时会打印两个值——它们依赖 Debug trait。所以被比较的值必须实现了 Debug(基本类型和大多数标准类型默认已实现)。

使用 Result 的测试

测试函数也可以返回 Result,这样可以使用 ? 运算符:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        let result = 2 + 2;
        if result == 4 {
            Ok(())
        } else {
            Err(String::from("2+2 不等于 4"))
        }
    }
}

集成测试

集成测试放在项目根目录的 tests/ 下,每个文件作为一个独立的 crate,只能测试公开 API:

my-project/
  src/
    lib.rs
  tests/
    integration_test.rs
    common/
      mod.rs            # 共享的测试辅助模块
// tests/integration_test.rs
use my_crate;  // 把我们的库当作外部 crate 引入

#[test]
fn test_public_api() {
    let result = my_crate::add(2, 3);
    assert_eq!(result, 5);
}

只运行集成测试:

cargo test --test integration_test

文档测试

Rust 的文档注释 /// 中的代码块在 cargo test 时也会被执行——这确保文档永远不会过时:

/// 将两个数字相加。
///
/// # 示例
///
/// ```
/// let result = my_crate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(left: usize, right: usize) -> usize {
    left + right
}
文档测试是 Rust 最被低估的特性之一。它强制文档中的代码示例保持正确——没有「Example 编译都通不过」的过时文档。这与 Go 的 Example 测试有相似之处。

测试组织最佳实践

// 测试私有函数:放在同一文件内的 tests 模块
#[cfg(test)]
mod tests {
    use super::*;  // 引入父模块的所有内容(包括私有函数)

    #[test]
    fn test_private_helper() {
        assert_eq!(private_helper(5), 10);
    }
}

fn private_helper(x: i32) -> i32 {
    x * 2
}

一句话小结

单元测试用 #[test] + assert! 系列宏,放在源码文件的 #[cfg(test)] mod tests {} 中。集成测试在 tests/ 目录。文档测试用 /// 注释,让代码示例即是测试。cargo test 一键运行所有测试。


Rust 基础系列到此完结。掌握了这 11 篇内容,你已经覆盖了 Rust 核心语言的大部分知识点。接下来可以深入 Web 框架并发与异步 等具体领域。

练习

  1. 为以下函数编写单元测试(正常情况和 panic 情况都要覆盖):
fn safe_reciprocal(x: f64) -> Result<f64, String> {
    if x == 0.0 {
        Err(String::from("不能计算零的倒数"))
    } else {
        Ok(1.0 / x)
    }
}
  1. safe_reciprocal 添加文档注释,并包含一个可执行的文档测试。
参考答案
/// 计算一个数的倒数。
///
/// # 示例
///
/// ```
/// let result = safe_reciprocal(2.0);
/// assert_eq!(result, Ok(0.5));
/// ```
///
/// # 错误
///
/// 当输入为 0.0 时返回错误。
///
/// ```
/// let result = safe_reciprocal(0.0);
/// assert!(result.is_err());
/// ```
fn safe_reciprocal(x: f64) -> Result<f64, String> {
    if x == 0.0 {
        Err(String::from("不能计算零的倒数"))
    } else {
        Ok(1.0 / x)
    }
}

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

    #[test]
    fn test_normal_case() {
        let result = safe_reciprocal(2.0);
        assert_eq!(result.unwrap(), 0.5);
    }

    #[test]
    fn test_large_number() {
        let result = safe_reciprocal(100.0);
        assert!((result.unwrap() - 0.01).abs() < 0.001);
    }

    #[test]
    #[should_panic(expected = "不能计算零的倒数")]
    fn test_zero_panics() {
        safe_reciprocal(0.0).unwrap();
    }
}
最后更新于