Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.17.3. 路径(Path)Pt.2

然后看下面的测试函数。首先声明了两个矩形largersmaller,分别存储大矩形和小矩形的宽高,这就是准备(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_largersmaller.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格式打印值,所以参数必须实现PartialEqDebug这两个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_twoa + 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。失败信息显示left5(也就是add_two(2)的结果),right4

另外还有assert_ne!:两个值不相等时通过,相等时失败。它最适合用在你不确定具体会得到什么值、但知道它绝对不该是某个值的场合。