11.2 断言(Assert)
11.2.1. 使用assert!宏检查测试结果
assert!宏来自标准库,用于判断某个条件是否为true。它接收一个返回类型为布尔值的表达式:
- 当
assert!内的值为true时,测试通过,assert!也不会做多余的操作。 - 当
assert!内的值为false时,assert!会调用panic!,测试失败。
看个例子:
#![allow(unused)]
fn main() {
#[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
}
}
}
结构体Rectangle存储矩形的宽和高。它定义了can_hold方法,用于判断一个矩形能否容纳另一个矩形(不考虑斜着放)。逻辑很好理解:只要看当前矩形的宽和高是否都大于另一个矩形即可。
该如何测试这个方法呢?因为它的返回类型正好是bool,所以用assert!再合适不过:
#![allow(unused)]
fn main() {
#[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));
}
}
}
由于test是一个模块,所以test模块内如果想使用外部的内容,就必须先导入到当前作用域。这里写的是use super::*;,*会把外部模块的所有内容导入进test模块。有关这部分的详细内容,可以看 7.2. 路径(Path)Pt.1 和 7.3. 路径(Path)Pt.2。
然后看下面的测试函数。首先声明了两个矩形larger和smaller,分别存储大矩形和小矩形的宽高,这就是准备(Arrange)阶段。
下面的assert!宏调用了can_hold,这就是运行(Act)阶段。
最后用assert!来判断测试是否成功。
在这个例子中,larger存储的宽高绝对可以容纳smaller,所以结果一定是true,测试通过。
运行cargo test:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.12s
Running unittests src/lib.rs (target/debug/deps/rectangle-2f89d610a9fe6c00)
running 1 test
test tests::larger_can_hold_smaller ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
那如果小矩形容纳不了大矩形呢?
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
//...
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
}
又声明了另一个测试函数smaller_cannot_hold_larger。smaller.can_hold(&larger)一定返回false,但前面加了取反运算符!,所以最终assert!收到的仍然是true,测试通过:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.08s
Running unittests src/lib.rs (target/debug/deps/rectangle-2f89d610a9fe6c00)
running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
两个测试都能通过,说明can_hold方法大概没问题。
现在改一下这个方法,把can_hold中的宽度比较从>改成<:
#![allow(unused)]
fn main() {
#[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
}
}
}
逻辑现在就错了。再运行同样的测试函数:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.07s
Running unittests src/lib.rs (target/debug/deps/rectangle-2f89d610a9fe6c00)
running 2 tests
test tests::smaller_cannot_hold_larger ... ok
test tests::larger_can_hold_smaller ... FAILED
failures:
---- tests::larger_can_hold_smaller stdout ----
thread 'tests::larger_can_hold_smaller' (454276) panicked at src/lib.rs:28:9:
assertion failed: larger.can_hold(&smaller)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::larger_can_hold_smaller
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
有一个测试失败了,说明错误被成功捕获了。这也是编写测试的目的:尽早发现问题。
11.2.2. 使用assert_eq!和assert_ne!测试相等性
assert_eq!中的eq指的是equal(相等),assert_ne!中的ne指的是not equal(不相等)。这两者都来自标准库。
这两个宏接收两个参数,并判断这两个值是否相等。通常把被测试代码的结果作为一个参数,把期待的结果作为另一个参数,然后宏就会检查这两个结果是否相等。
实际上,这两个宏的用法很像==和!=运算符。不同之处在于,如果失败,它们会自动打印出两个参数的值,从而帮助开发者理解测试失败的原因。
使用这两个宏有一定要求。它们用debug格式打印值,所以参数必须实现PartialEq和Debug这两个trait。所有基本类型和大部分标准库类型都已经实现了,但自定义结构体和枚举必须自行实现这些trait。这两个trait都是可派生的,所以对自定义类型通常只要这样写即可:
#![allow(unused)]
fn main() {
#[derive(PartialEq, Debug)]
struct Point {
x: i32,
y: i32,
}
}
加上这个标注后,就可以用assert_eq! / assert_ne!比较Point值,并且断言失败时能够打印出这些值。
下面是一个使用assert_eq!的例子:
#![allow(unused)]
fn main() {
pub fn add_two(a: usize) -> usize {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
}
}
add_two函数会给参数加2。测试函数it_adds_two调用了add_two;因为2 + 2 = 4,所以期待的add_two(2)的值是4,把4和函数调用放进宏里即可。其实在Rust中,期待的值和函数调用的位置是可以互换的。有些语言对顺序有明确要求,但Rust没有。放在左边(第一个参数)的值只是叫做左值,另一个叫做右值。
输出:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.07s
Running unittests src/lib.rs (target/debug/deps/adder-302521ba8d0f0bdf)
running 1 test
test tests::it_adds_two ... 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
接下来引入一个逻辑错误,把add_two的a + 2改成a + 3,其余不变,看看会发生什么:
#![allow(unused)]
fn main() {
pub fn add_two(a: usize) -> usize {
a + 3
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
}
}
输出:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.07s
Running unittests src/lib.rs (target/debug/deps/adder-302521ba8d0f0bdf)
running 1 test
test tests::it_adds_two ... FAILED
failures:
---- tests::it_adds_two stdout ----
thread 'tests::it_adds_two' (455023) panicked at src/lib.rs:12:9:
assertion `left == right` failed
left: 5
right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_adds_two
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
测试抓住了这个bug。失败信息显示left是5(也就是add_two(2)的结果),right是4。
另外还有assert_ne!:两个值不相等时通过,相等时失败。它最适合用在你不确定具体会得到什么值、但知道它绝对不该是某个值的场合。