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!内的值为falseassert!就会调用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.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

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.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

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.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok

failures:

---- tests::larger_can_hold_smaller stdout ----
thread 'tests::larger_can_hold_smaller' 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这两个traits。所有基本类型和大部分标准库类型都实现了,只不过对于自定义的结构体和枚举来说就得自行实现这两个trait。

自行实现Display trait的例子:

use std::fmt;

// 定义一个结构体
struct Point {
    x: i32,
    y: i32,
}

// 为 Point 实现 Display trait
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // 定义格式化的输出
        write!(f, "Point(x: {}, y: {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 10, y: 20 };
    println!("{}", p); // 使用 Display 格式化输出
}

看个使用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.58s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

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.61s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... FAILED

failures:

---- tests::it_adds_two stdout ----
thread 'tests::it_adds_two' 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`

失败的时候它把两个值打印出来了,左值是4,右值是5。

现在4和5不相等,那么这时候把测试函数的assert_eq!改成assert_ne!就能够通过测试。