Rast编写自动化测试

Rust中的测试函数是用来验证非测试代码是否按照期望方式运行的。一般执行以下三种操作:

  • 设置任何所需的数据或状态
  • 运行所需要测试的代码
  • 断言其结果是我们所期望的

测试函数剖析🔗

新建一个库项目,它的src/lib.rs内容如下:

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

我们关注it_works函数,fn行之前的#[test]表明这是一个测试函数。这里使用assert_eq!宏来断言result的值等于4。

使用cargo test命令来运行测试,结果如下:

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests testdd

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

可以看见我们通过了测试。

下面我们添加一个测试失败的例子,当测试函数出现panic时测试就失败了。

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn exploration() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}

再次运行cargo test,结果如下:

running 2 tests
test tests::it_works ... ok
test tests::another ... FAILED

failures:

---- tests::another stdout ----

thread 'tests::another' panicked at src/lib.rs:17:9:
Make this test fail
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::another

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`

这里可以看见一个测试通过,一个测试失败。

使用宏来检查结果🔗

assert!由标准库提供,需要向assert!提供一个布尔值的参数。如果时间为true,就什么都不会做,如果是false,就会调用panic!宏。

assert_eq!和assert_ne!宏会比较两个值是否相等,两个宏都有两个参数,如果两个参数相等时,assert_eq!什么都不会做,assert_ne!会调用panic!宏,如果两个参数不相等时,assert_eq!会调用panic!宏,assert_ne!什么都不会做。
由于assert_eq!和assert_ne!宏底层分别使用了==和!=操作符,所以被比较的值必须实现了PartialEq和Debug这两个trait。

可以向assert!、assert_eq!和assert_ne!提供一个可选的失败信息参数,可以在测试失败时将自定义失败信息打印出来。在这三个函数必要的参数后面添加的参数都会传递给format!宏。

使用should_panic检查panic🔗

对函数增加另一个属性should_panic使这个函数在panic时通过,在没有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);
    }
}

#[should_panic]属性位于#[test]属性的下面之后。

然而,should_panic测试结果也可能含糊不清,在一些不是我们期望的原因而导致的panic时,测试也会通过。为了让测试更清晰,我们可以增加expected参数:

pub struct Guess {
    value: i32,
}
impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1  {
            panic!("Guess value must be greater than or equal to 1, got {}.", value);
        } else if value > 100 {
            panic!("Guess value must be less than or equal to 100, got {}.", value);
        }
        Guess { value }
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

这个测试会通过,因为panic信息包含了less than or equal to 100。

将*Result<T,E>*用于测试🔗

我们可以用Result<T,E>编写测试,这样在失败时可以返回Err而不是panic:

pub fn add(left: usize, right: usize) -> usize {
    left + right
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn it_works() -> Result<(), String> {
        let result = add(2, 2);
        if result == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

这个函数体中没有调用assert_eq!宏,而是直接返回一个Result<T,E>。如果测试通过,就返回Ok(()),如果测试失败,就返回Err。

不能对Result<T,E>使用#[should_panic]注解,应该使用assert!宏,像这样:assert!(value.is_err())。