编写自动化测试
如何编写测试
测试是一个函数,用于验证非测试代码是否按照期望的方式运行
函数体一般包括三部分
- 准备所有的数据或状态
- 调用需要测试的代码
- 断言运行结果与我们期望的一致
测试函数的构成
- 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访问核心功能,使用一些胶水代码进行工作