Rust 单元测试详细教程

一、Rust 单元测试详细教程

1、基本概念

在 Rust 中,单元测试直接写在源代码文件中,通常位于 #[cfg(test)] 标记的模块内。测试函数需用 #[test] 属性标记。

// 被测试函数
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// 测试模块
#[cfg(test)]
mod tests {
    use super::*; // 引入外部函数

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5); // 断言测试
    }
}

2、核心组件

(1) 断言宏

  • assert!(expr):表达式为真则通过
  • assert_eq!(left, right):验证相等
  • assert_ne!(left, right):验证不等
  • #[should_panic]:测试预期 panic
#[test]
#[should_panic(expected = "除数不能为零")]
fn test_divide_by_zero() {
    divide(10, 0); // 预期触发 panic
}

(2) 测试模块组织

#[cfg(test)]
mod tests {
    // 测试私有函数
    #[test]
    fn test_private() {
        assert_eq!(internal_logic(), 42);
    }
    
    // 子测试模块
    mod edge_cases {
        #[test]
        fn test_negative() {
            assert_eq!(add(-1, -1), -2);
        }
    }
}

3、 高级技巧

(1) 测试初始化

#[cfg(test)]
mod tests {
    // 共享初始化代码
    fn setup() -> String {
        String::from("test_data")
    }

    #[test]
    fn test_data_processing() {
        let data = setup();
        assert!(!data.is_empty());
    }
}

(2) 忽略测试

#[test]
#[ignore = "性能测试暂不运行"]
fn heavy_computation_test() {
    // 耗时操作
}

(3) 条件编译

#[test]
#[cfg(feature = "network")]
fn network_api_test() {
    // 仅当启用 network 特性时编译
}

4、测试运行控制

(1) 运行特定测试

cargo test test_add  # 运行单个测试
cargo test add       # 运行名称包含"add"的测试

(2) 并行控制

cargo test -- --test-threads=1  # 单线程运行

(3) 显示输出

cargo test -- --nocapture  # 显示 println! 输出

5、最佳实践

  1. 测试覆盖率:使用 tarpaulin 工具

    cargo tarpaulin --ignore-tests
    
  2. 测试私有函数:直接测试模块内部实现

  3. BDD 风格:使用 speculoos

    use speculoos::*;
    #[test]
    fn test_list() {
        let list = vec![1, 2, 3];
        assert_that(&list).has_length(3);
    }
    
  4. Mock 对象:使用 mockall

    #[automock]
    trait DataSource {
        fn fetch(&self) -> u32;
    }
    
    #[test]
    fn test_mock() {
        let mut mock = MockDataSource::new();
        mock.expect_fetch().returning(|| 42);
        assert_eq!(mock.fetch(), 42);
    }
    

6、添加自定义消息

在 Rust 测试中添加自定义消息可通过断言宏的额外参数实现,以下是具体方法和示例:

1、 assert! 宏添加消息

#[test]
fn test_basic() {
    let result = 2 + 2;
    assert!(result == 4, "加法计算错误,得到 {} 但期望 4", result);
}

当断言失败时输出:

thread 'test_basic' panicked at '加法计算错误,得到 5 但期望 4'

2、assert_eq! 宏添加消息

#[test]
fn test_equality() {
    let actual = "hello".to_string();
    let expected = "world";
    assert_eq!(actual, expected, "字符串不匹配: 实际='{}', 期望='{}'", actual, expected);
}

失败时输出:

thread 'test_equality' panicked at '字符串不匹配: 实际='hello', 期望='world''

3、使用 format! 构建复杂消息

#[test]
fn test_complex() {
    let vec = vec![1, 2, 3];
    assert!(
        vec.len() > 5,
        "向量长度不足: 长度={}, 内容={:?}",
        vec.len(),
        vec
    );
}

4、最佳实践

  1. 清晰说明:明确说明期望值和实际值
    assert!(user.is_admin(), "用户 {} 无管理员权限", user.id)
    
  2. 包含关键数据:在消息中输出相关变量值
  3. 避免敏感信息:不要在消息中包含密码等敏感数据
  4. 使用格式规范
    assert_eq!(result, 3.14159, "精度不足: {:.5} vs {:.5}", result, 3.14159)
    

5、错误消息原理

Rust 的断言宏本质上是:

macro_rules! assert {
    ($cond:expr, $($msg:tt)*) => {
        if !$cond {
            panic!($($msg)*)
        }
    }
}

自定义消息通过 panic! 宏的格式化能力实现,支持所有标准格式语法。

7、常见问题解决

  1. 测试未编译:检查 #[cfg(test)] 是否遗漏
  2. 无法访问私有项:使用 use super::*; 引入父模块
  3. I/O 测试失败:使用 tempfile 库处理临时文件
  4. 异步测试:使用 #[tokio::test] 属性

二、代码示例

1、正常通过

测试源码:

pub struct Counter {
    value: i32,
}

impl Counter {
    pub fn new() -> Self {
        Counter { value: 0 }
    }

    pub fn increment(&mut self) {
        self.value += 1;
    }

    pub fn get(&self) -> i32 {
        self.value
    }
    pub fn add(&mut self, n: i32) {
        self.value += n;
    }
}

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

    #[test]
    fn test_counter_initial() {
        let c = Counter::new();
        assert_eq!(c.get(), 0);
    }

    #[test]
    fn test_counter_increment() {
        let mut c = Counter::new();
        c.increment();
        assert_eq!(c.get(), 1);
    }
    #[test]
    fn test_counter_add() {
        let mut c = Counter::new();
        c.add(5);
        assert_eq!(c.get(), 5,"test add failed! please check your code");
    }

    #[test]
    #[should_panic]
    fn test_overflow() {
        let mut c = Counter::new();
        for _ in 0..(i32::MAX as u32 + 1) {
            c.increment(); // 
        }
    }
}

测试结果:

PS G:\Learning\Rust\unitTest> cargo test
   Compiling unitTest v0.1.0 (G:\Learning\Rust\unitTest)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running unittests src\main.rs (target\debug\deps\unitTest-cb66f5a5bbe86c70.exe)

running 4 tests
test tests::test_counter_add ... ok
test tests::test_counter_increment ... ok
test tests::test_counter_initial ... ok
test tests::test_overflow - should panic ... ok

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

PS G:\Learning\Rust\unitTest> 

在这里插入图片描述

2、异常代码

测试代码:

pub struct Counter {
    value: i32,
}

impl Counter {
    pub fn new() -> Self {
        Counter { value: 0 }
    }

    pub fn increment(&mut self) {
        self.value += 1;
    }

    pub fn get(&self) -> i32 {
        self.value
    }
    pub fn add(&mut self, n: i32) {
        self.value += n;
    }
}

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

    #[test]
    fn test_counter_initial() {
        let c = Counter::new();
        assert_eq!(c.get(), 0);
    }

    #[test]
    fn test_counter_increment() {
        let mut c = Counter::new();
        c.increment();
        assert_eq!(c.get(), 1);
    }
    #[test]
    fn test_counter_add() {
        let mut c = Counter::new();
        c.add(5);
        assert_eq!(c.get(), 6,"test add failed! please check your code");
    }

    #[test]
    #[should_panic]
    fn test_overflow() {
        let mut c = Counter::new();
        for _ in 0..(i32::MAX as u32 + 1) {
            c.increment(); // 
        }
    }
}

测试结果:

PS G:\Learning\Rust\unitTest> cargo test
   Compiling unitTest v0.1.0 (G:\Learning\Rust\unitTest)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running unittests src\main.rs (target\debug\deps\unitTest-cb66f5a5bbe86c70.exe)

running 4 tests
test tests::test_counter_add ... FAILED
test tests::test_counter_increment ... ok
test tests::test_counter_initial ... ok
test tests::test_overflow - should panic ... ok

failures:

---- tests::test_counter_add stdout ----

thread 'tests::test_counter_add' panicked at src\main.rs:42:9:
assertion `left == right` failed: test add failed! please check your code
  left: 5
 right: 6
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::test_counter_add

test result: FAILED. 3 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.83s

error: test failed, to rerun pass `--bin unitTest`
PS G:\Learning\Rust\unitTest> 

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小灰灰搞电子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值