前言#
昨天我们学完了生命周期,那么第十章就学完了
接下来是第十一章
编写自动化测试#
有个点得写在最前:不要相信和你对接的测试,即使再牛逼也不要完全信赖测试。(当然,如果你的测试能包揽你的工作并且非常出色,那就当我没说,因为他可能甚至把你的代码也看了)。
相信你也用过一些测试相关的插件。
以前端为例,比如jest之类的。
怎么说呢?如果是业务逻辑相关的,那就有些鸡肋了。比较可能涉及到了多逻辑嵌套等问题,写一堆代码可能效率还不如手动去浏览器上点击。
但如果你是在写插件,在写框架,那就完全不同了。毕竟你可能入口都还没写,如果可以一小块一小块的测试就会很舒服,效率就会比较高。
而在rust中,rust自身是非常看重correctness也就是正确性的,也可说是安全。
因为不容易报错出问题和安全的意思差不多。这里面type system也就是类型系统承担了相当大的一部分负担(也就是对correctness来说起了很大作用)。
但是它也有做不到的地方,这一点任何语言都一样。
举个栗子:我们准备实现一个函数add_two,它接收一个整数并返回整数加二之后的结果。
fn main () {
println!("{}", add_two(2)); // 4
}
fn add_two (num: i32) -> i32 {
num + 3
}这块代码肯定是不会报错的,但是结果是错的,我们想要的是2 + 2,而实际结果是2 + 3。
这就是rust做不到的地方,它的编译器能帮你确认输入输出的类型,确认生命周期是否正常,但是就是不能帮你看你的代码是怎么写的,你写成什么样子,它不知道,它也不想知道。
简单地说,如果错误来自业务逻辑,那么不好意思,请你自己反省为什么会写出这种代码。
所以为了让你写出来的代码不会丢rust的脸, rust提供了对软件自动化测试的支持。你可以用它来测试你的代码逻辑。
如果你接触过单元测试等,你应该会发现它们通常都有以下三种特点
- 需要定义测试数据或者状态,包括最终的数据数据/状态。
- 它们跑的代码可以是指定的某一块或者某一个函数。
- 断言(
assert)运行结果是否符合预期。
测试函数#
rust中的test也是基于函数function的,只是添加了注释用来区分是否是测试代码。
rust把这种注释叫做attribute,属性是关于rust代码片段的元数据(如果你看我的文章发现有些话拗口压根看不懂,那就说明我在直接翻译。。。),我们之前就遇到过了。比如:
#[derive(Debug)] 这里的derive就是一个attribute。
所以参照dereive,注释一个test用的是#[test],比如下面这个例子
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
} 有个点需要注意,那就是必须注释在fn的上面,和struct需要println!时引入dereive放在struct前面一样。
然后我们终端执行cargo test

当我们执行cargo test时,编译器会生成test runner binary,然后执行所有test。
输出和其它语言中的很像,最后有个·summary。
ok自然表示测试通过,而failed表示测试不通过。
ignored后面我们会遇到,毕竟我们大部分时候只是想测试一个而已。
至于measure..我也不知道是啥,先mark下。
你应该还注意到了一个点Doc-tests,我们并没有包含任何的文档测试(documentation test) ,我们后面会学到。
如果创建的项目是library的话,那么会自动带有test,这也就是我说的在库中使用会更能发挥作用。
我们来创建一个新的项目(day3的一直用到现在。。。)
cd ../../
mkdir day23
cd day23
cargo new adder --lib
我们来写个错误的例子
#[test]
fn another() {
panic!("Make this test fail");
}
可以看到输出了错误的结果Make this test fail。
使用assert!来检查数据#
看到!,那就可以说明这是一个macro。这个宏在之后会非常有用,它接收一个布尔值作为参数,当true的时候表示你这个测试单元ok没问题,但是如果你这个参数是false的话,就会调用panic!,将这个测试单元判定为false。
我们来看个例子
#[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));
}
}
super我们之前说过的,表示可以使用上一层作用域的struct等。

assert!接收larger.can_hold(&smaller)的返回结果作为参数。
结果为true,测试没问题。
然后我们反过来
assert!(smaller.can_hold(&larger));期望的结果应该是false

也是符合预期。
assert_eq!和assert_ne!#
这个assert!虽然方便,但是也并不是所有时候都适用,准确的说是大部分时候都不适用,我们有时候需要调用function,而function的结果往往不会是一个bool,所以这个时候就需要有数据可以比对,这样才能知道结果是否符合预期。
当然,你也可以使用==比较符,但是不如下面这两个方法优雅。
这个assert_eq!你之前应该看到过,eq是equal的缩写。那么ne自然就是no equal的缩写。
他俩和assert!是差不多的。
来看下assert_eq的源码,可以看到接收left和right两个参数,其实就是比对这两者的数据是否一样。

直接来看下例子
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
assert_eq!(4, add_two(2));
}
}结果自然是符合预期的2 + 2 == 4。
ok的场景不看了,我们来看下fail的。

assertion failed: (left == right)。俩参数的结果也有给出,很方便找出问题。
而assert_ne!我们就不看了,和assert_eq!相反。
有个点需要注意,那就是两个数据要能对比,同时还要能被调试输出,那么就意味着它俩得实现PartialEq和Debug这俩trait才行。
不过好在大多是标准库里的类型都有impl这俩trait。
不过enum和struct你得自己去impl才行,之前讲过了,它俩压根不知道是什么格式,数据可以各种类型,也就是无法确定每一个enum/struct格式能长得一样,所以rust并不能给它们impl这个Debug和PartialEq。
不过,根据我们之前使用Debug的用法,我们可以在struct顶部注释一个derive的属性将他俩derive(只有derivable的才能被derive)进入当前上下文。
#[derive(PartialEq, Debug)]自定义错误语句#
我们直接来看下例子
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`",
result
);
}
}可以看到assert!接收了三个参数,我们来跑下,期望是failed

可以看到我们传入的第二个第三个参数被输出了。
实际上它们被format!了,这三个assert方法默认都是把后面几个参数给format!,然后作为错误提示语句。
当然,后面的参数有多少个都是可以的,但是这里面的第一个参数必须要是一个带有{}的字符串,这样才能format!。 有几个参数就来几个{}。
assert!(
result.contains("Carol"),
"Greeting did not contain {}, value was `{}`",
"Carol",
result
);
使用should_panic来检查panics#
还记得我们前几天学的“啥时候需要用panic”吗?
在test中我们需要特殊处理,为什么呢?
因为有时候panic就是你期望的结果,这样就有冲突了。
来看下例子
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}我们在greater_than_100的fn前面注释了#[should_panic],然后我们调用new方法,但是数据不符合1-100,按理说是会panic。

这是非测试阶段正常调用的结果,符合我们的结果。
但是,在test中,报错才是我们的expect,所以应该是ok的状态才行。
现在我们加上#[should_panic]之后

这回符合预期了。
在test中使用Result#
表示对错,大家应该想到了更加优雅的Result,在test中也是支持的。
现在我们来试下
#[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"))
}
}
}
比起assert_eq!传入参数的方式,通过Err变体传入错误语句会方便很多。当然,你还可以用更方便的?操作符,这样就更优雅了。
不过有一点需要注意,那就是如果使用了Result,那就不能使用#[should_panic] 。还有就是Result不能作为返回值,也就是返回语句里不能使用?或者手动return一个Result。
当然,你可以使用assert!(res.is_err())代替,is_err会返回一个布尔值。
总结#
今天有点啰嗦,如果你之前学过自动化测试等,那么这一章完全没有难度。
最后,如果这篇文章对你有帮助,请务必点个赞,谢谢·~
参考#
- ^rust-wiriting-automated-tests https://doc.rust-lang.org/book/ch11-00-testing.html#writing-automated-tests
- ^rust-the-anatomy-of-a-test-function https://doc.rust-lang.org/book/ch11-01-writing-tests.html#the-anatomy-of-a-test-function
- ^rust-use-assert!-macro-to-check-data https://doc.rust-lang.org/book/ch11-01-writing-tests.html#checking-results-with-the-assert-macro
- ^rust-define-custom-failure-messages https://doc.rust-lang.org/book/ch11-01-writing-tests.html#adding-custom-failure-messages
- ^rust-checking-panics-with-should_panic https://doc.rust-lang.org/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic
- ^rust-use-Result-in-test https://doc.rust-lang.org/book/ch11-01-writing-tests.html#using-resultt-e-in-tests
发布于 2022-12-18 22:52・IP 属地广东
