标题
一、如何编写测试
1.1 一些概念
Rust 中的测试函数体通常执行如下三种操作:
- 设置任何所需的数据或状态;
- 运动需要测试的代码;
- 断言其结果是我们所期望的;
Rust提供的专门用来编写测试的功能:
- test属性;
- 一些宏;
- should_panic属性;
1.2 测试函数剖析
- Rust中的测试就是一个在fn行之间加上
#[test]
属性标注的函数; - cargo test命令执行时,会运行被标记了#[test]的函数,并报告是否运行通过;
创建新的库项目
cargo new adder --lib
- 源码只有
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);
}
}
- fn函数之前的
#[test]
表明这是一个测试函数; - tests是一个普通的内部模块,要测试外部的中的add函数,因此要用use引入;
- 函数体通过使用
assert_eq!
宏来断言2加2等于4,这是测试用例的典型格式; - 使用
cargo test
运行测试用例;
- 结果显示:1 passed; 0 failed;…… 说明所有测试都通过了;
- tests::it_works中的it_works是测试函数的名字;
- ignored表示忽略的测试用例,filtered out表示过滤需要运行的测试,measured是针对性能测试的(目前只能用于Rust开发版(nightly Rust,详阅https://doc.rust-lang.org/unstable-book/library-features/test.html);
- Doc-tests adder 是所有文档测试的结果;
再加一个失败的测试项
#[cfg(test)]
mod tests {
……
#[test]
fn another() {
panic!("Make this test fail");
}
}
运行时会提示it_works运行成功而another运行失败;
1.3 使用assert!宏检查结果
assert!
宏由标准库提供,在希望确保测试中一些条件为true时非常有用;- 需要向
assert!
宏提供一个求值为布尔值的参数; - 如果值是true,则什么也不做,同时测试通过;
- 如果值是false,
assert!
宏调用panic!
宏,同时测试失败; - 在
assert!
最后附上错误提示字符串,以便更好定位 (可选);
为前面写的Rectangle结构体和一个can_hold添加一个测试用例,看看8*7的实例能否放下5*1的实例;
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle { width: 8, height: 7 };
let smaller = Rectangle { width: 5, height: 1 };
assert!(larger.can_hold(&smaller), "运行失败!");
}
}
测试结果为通过
1.4 使用assert_eq!和assert_ne!宏来测试相等
- 标准库提供
assert_eq!
和assert_ne!
宏比较相等或不相等; - 当断言失败时也会打印出这两个值具体是什么,以便于观察测试为什么失败;
- 依然可以自定义断言失败时候的输出信息;
- 宏参数中的左右两个值的顺序并不重要;
1) assert_eq!
fn add_two(a: i32) -> i32{
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(4, result);
}
#[test]
fn it_adds_two2() {
let result = add_two(1);
assert_eq!(4, result, "4 is not equal {}.", result);
}
}
输出结果
- it_add_two运行通过;
- it_adds_two2运行失败,左值为4右值为3,因此不相等,断言失败;
- 输出了我们自己定义的错误提示信息:4 is not equal 3。
2) assert_ne!
assert_ne!
宏在传递给它的两个值不相等时通过;- 在代码按预期运行,我们不确定值会是什么,不过能确定值绝对不会是什么的时候,这个宏最有用处;
TIPS:
- 如果一个函数保证会以某种方式改变其输出,不过这种改变方式是由运行测试时是星期几来决定的,这时最好的断言可能就是函数的输出不等于其输入。
- assert_eq! 和 assert_ne! 宏在底层分别使用了 == 和 !=。
- 当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了PartialEq 和 Debug trait。
- 对于自定义的结构体和枚举,需要实现PartialEq才能断言他们的值是否相等。
- 需要实现Debug才能在断言失败时打印他们的值。
- 由于这两个 trait 都是派生 trait,因此通常可以直接在结构体或枚举上添加
#[derive(PartialEq, Debug)]
标注。 - 更多关于这些和其他派生 trait 的详细信息请参阅 这里 ;
1.5 使用 should_panic 检查 panic
- 可以对测试程序添加
#[should_panic]
属性; - 即使满足条件也不会触发程序中的
panic!
宏;
fn add_two(a: i32) -> i32{
if a < 0 {
panic!("Par a must larger 0 but a is {}", a);
}
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn it_adds() {
let result = add_two(-5);
assert_eq!(4, result);
}
}
- 当add_two传入的值小于0时会触发
panic!
宏; - 但是给it_adds额外添加一个
#[should_panic]
属性时依然会通过测试;
缺点
- 添加
should_panic
属性之后的测试结果只是告诉我们代码产生了 panic; should_panic
在一些不是我们期望的原因而导致panic时也会通过 ;- 可以添加
expected
参数来关注我们所期待导致的panic;
fn add_two(a: i32) -> i32{
if a < 0 {
panic!("Par a must larger than 0 but a is {}", a);
}else if a > 100{
panic!("Par a must less than 100 but a is {}", a);
}
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Par a must larger than 0 but a is")]
fn it_adds() {
let result = add_two(101);
assert_eq!(4, result);
}
}
- 为it_adds添加的
[should_panic(expected = "Par a must larger than 0 but a is")]
属性代表只有当panic!宏提示的字符串不包含Par a must larger than 0 but a is
时会出错。 - 传给add_two函数的值是101,因此走的是else if分支,panic!宏提示的错误是Par a must less than 100 but a is,这不包含expected中的子串,因此会报错。
二、将 Result<T, E> 用于测试
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
- it_works 函数的返回值类型为
Result<(), String>
; - 测试通过时返回 Ok(()),在测试失败时返回带有 String 的 Err;
- 这样编写测试来返回 Result<T, E> 就可以在函数体中使用问号运算符,如此可以方便地编写测试,如果其中的任何操作返回 Err 值,则测试将失败;
- 不能使用
#[should_panic]
,要断言操作返回 Err 值,需要使用assert!(value.is_err())
;