《Rust权威指南》 第11章 编写自动化测试

如何编写测试

测试是一个函数,用于验证非测试代码是否按照期望的方式运行
函数体一般包括三部分

  • 准备所有的数据或状态
  • 调用需要测试的代码
  • 断言运行结果与我们期望的一致

测试函数的构成

  • Rust中的测试就是一个标注有#[test]属性的函数
  • 编写测试完成后,可以使用cargo test命令类运行测试

使用

cargo new adder --lib

创建一个adder库,该库会自动生成一个src/lib.rs文件,看起来是下面这样

pub fn add(left: usize, right: usize) -> usize {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

使用cargo test运行测试,得到结果:

PS E:\Rust\Demo\adder> cargo test
   Compiling adder v0.1.0 (E:\Rust\Demo\adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.70s
     Running unittests src\lib.rs (target\debug\deps\adder-0a86cd050490705e.exe)

running 1 test
test tests::it_works ... ok

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

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
  • running 1 test表示当前正在执行1个测试
  • 接下来一行显示所生成的册数函数名称it_works和相应的测试结果ok
  • 下一行是该测试集的摘要
  • 再往下是文档测试的结果,暂且不谈

总的来说,一旦测试函数触发panic,则该测试视为失败

assert!宏

assert宏接收一个返回布尔类型的表达式或函数作为参数

  • 当true,通过测试
  • 当false,调用panic!宏,测试失败

asser_eq!宏和assert_ne!宏

该两个宏都会接收两个参数,分别比较两个参数是相等还是不相等

  • 当断言失败时,还会自动打印出两个参数的值

根据以上特征,意味着这两个宏的参数必须实现PartialEq和Debug这两个trait
所有基本类型和大部分标准库类型是满足的
对于自定义和结构体,可以在定义上一行加#[derive(ParitialEq,Debug)]

添加自定义的错误提示消息

任何assert家族的必要参数之后出现的参数一起被传递给format!宏,并最终作为附加信息打印在测试结果里面

pub fn add(left: usize, right: usize) -> usize {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 3);
        assert_eq!(result, 4,"result is {} ",result);
    }
}

此时测试失败的时候,会显示我们的附加信息

thread 'tests::it_works' panicked at 'assertion failed: `(left == right)`
  left: `5`,
 right: `4`: result is 5 ', src\lib.rs:12:9

使用should_panic检查panic

为测试函数额外添加属性#[should_panic],会使得测试函数在发生panic时测试通过,不发生panic测试失败

pub struct Num {
    value:i32,
}

impl Num {
    pub fn new(value:i32) -> Num {
        if value < 1 || value > 100 {
            panic!("illigal value {} for Num!",value);
        }
        Num {
            value
        }
    }

    pub fn value(&self) -> i32 {
        self.value
    }
}


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

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Num::new(200);
    }
}

我们可以在should_panic属性中添加可选参数expected,这会检测panic发生时输出的错误信息是否包含了指定文字
如果包含了,才算做通过测试
这使得我们能够分辨不同的panic

#[should_panic(expected = "illigal value")]

使用Result<T,E>编写测试

与panic型的测试不同,使用Result<T,E>编写测试,就是让测试函数返回一个Result<T,E>
类型

  • 当函数返回了Ok,则测试通过
  • 返回了Err,测试未通过,并且打印Err携带的信息

比如

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(),String> {
        if 2+2 == 5 {
            Ok(())
        } else {
            Err(String::from("bad !"))
        }
    }
}

在进行acrgo test 后,由于该测试未通过,会显示

running 1 test
test tests::it_works ... FAILED

failures:

---- tests::it_works stdout ----
Error: "bad !"

若通过了,会显示ok

running 1 test
test tests::it_works ... ok

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

控制测试的运行方式

  • 使用cargo test --参数来指定命令行参数
  • 使用cargo test -- --参数来为生成的二进制文件指定参数

并行或串行的进行测试

运行多个测试时,默认是并行的。如果你的测试在并行下运行可能会发生问题,可以通过cargo test -- --test-thread=1 指定运行的线程数量,改成串行测试

显示函数输出

默认下,Rust测试库会在测试通过时截获所有被打印至标准输出的消息。(比如你在test函数中使用println!来打印一些信息,如果该测试通过,你根本无法在标准输出处看到这些信息)

禁用输出截获功能,通过:

cargo test -- --nocapture

只运行部分特定名称的测试

向cargo test传递测试名称指定运行的测试

  • 运行单个测试
  • 指定测试名称的一部分作为参数,任何匹配这一名称的测试都会得到执行
//任何名字中包含`add`的都会被执行
cargo test add

显式指定忽略某些测试

有些测试格外耗费时间或者资源

  • 通过在测试函数前加#[ignore]属性标记测试
  • 正常进行cargo test时,忽略被标记为ignore的测试,并且在测试摘要中指出有几个测试被忽略
  • 使用cargo test -- --ignored来单独运行被忽略的测试

测试的组织结构

单元测试

单元测试是将一小段代码隔离出来

  • 将单元测试和需要测试的代码放在src目录下的同一文件
  • 在这一文件中,新建一个被#[cfg(test)]标记的test模块来存放测试函数
    • 该标记的意义是可以让Rust只在执行cargo test时编译和运行该段代码;而在cargo build 时剔除他们

测试私有函数

Rust允许测试私有函数

也就是未声明为pub的函数,在test函数中也可以调用并接受测试

集成测试

集成测试是完全位于代码库之外的

  • 为了创建集成测试,先创建一个test目录
    • 在项目根目录下创建tests文件夹,和src文件夹并列
    • tests文件下每一个文件都是独立的包,所以在进行测试中,在每一个文件里,需要用use将我们测试的目标库导入
    • 该目录只会在cargo test时进行编译

例子,将我们本章第一个例子改成集成测试
文件结构如下:
在这里插入图片描述
将测试代码迁移到tests/test.rs

use adder::*;

#[test]
fn it_works() {
    let result = add(2, 2);
    assert_eq!(result, 4);
}

运行cargo test

running 0 tests

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

     Running tests\test.rs (target\debug\deps\test-1cb955fe190dc46e.exe)

running 1 test
test it_works ... ok

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

   Doc-tests adder

running 0 tests

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

此时结果的顺序是:单元测试、集成测试、文档测试

另外,可以使用cargo test --test 文件名来指定执行tests下的哪个文件的所有测试函数

在测试集中使用子模块

每一个tests目录文件会被编译为各自独立的包并在测试时运行,
而tests子目录中的文件不会被视作单独的包进行编译,不会再测试时输出
所以对于拆分模块的方法,与在src文件夹下不同(回忆一下,当时对于模块树的一级节点,用与模块同名的文件存储于和lib.rs同级别的目录下)
在tests目录下,我们将模块名.rs(我们在src目录下的做法)改为模块名/mod.rs,来构建树结构

比如我们想要创建一个common子模块,那么文件结构看起来像是下面这样

在这里插入图片描述
tests/test.rs

use adder::*;

mod common;

#[test]
fn it_works() {
    common::setup();
    let result = add(2, 2);
    assert_eq!(result, 4);
}

tests/common/mod.rs

pub fn setup() {
    //做一些事
}

二进制包的集成测试

  • 只有library crate才可以将函数暴露给其他包来运行,binary crate只用于独立执行(所以当然不能为src/main.rs进行测试)
  • 所以一般来说,Rust的项目逻辑编写在src/lib.rs文件中,这样可以对其进行测试。而main.rs只是使用use访问核心功能,使用一些胶水代码进行工作
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值