Rust 是一个相当注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有种类的错误。为此,Rust 也在语言本身包含了编写软件测试的支持。

编写一个叫做add_two的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的类型检查和借用检查,例如,这些检查会确保我们不会传递String或无效的引用给这个函数。Rust 所不能检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。

可以编写测试断言,比如说,当传递3add_two函数时,返回值是5。无论何时对代码进行修改,都可以运行测试来确保任何现存的正确行为没有被改变。

11.1编写测试

如何编写测试

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

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

测试函数剖析

作为最简单例子,Rust 中的测试就是一个带有test属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据;第五章中结构体中用到的derive属性就是一个例子。为了将一个函数变成测试函数,需要在fn行之前加上#[test]当使用cargo test命令运行测试时,Rust 会构建一个测试执行程序用来调用标记了test属性的函数,并报告每一个测试是通过还是失败。

创建一个新的库项目adder

$ cargo new adder --lib Created library `adder` project$ cd adder

新建后的默认代码是,判断加法

pub fn add(left: usize, right: usize) -> usize {left + right}#[cfg(test)]mod tests {use super::*;#[test]fn it_works() {let result = add(2, 2);assert_eq!(result, 4);}}

使用

cargo test

结果

Cargo 编译并运行了测试。在CompilingFinishedRunning这几行之后,可以看到running 1 test这一行。下一行显示了生成的测试函数的名称,它是it_works,以及测试的运行结果,ok。接着可以看到全体测试运行结果的摘要:test result: ok.意味着所有测试都通过了。1 passed; 0 failed表示通过或失败的测试数量。

因为之前我们并没有将任何测试标记为忽略,所以摘要中会显示0 ignored。我们也没有过滤需要运行的测试,所以摘要中会显示0 filtered out

0 measured统计是针对性能测试的。性能测试(benchmark tests)在编写本书时,仍只能用于 Rust 开发版(nightly Rust)。

测试输出中的以Doc-tests adder开头的这一部分是所有文档测试的结果。我们现在并没有任何文档测试,不过 Rust 会编译任何在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!

改变测试的名称并看看这如何改变测试的输出。修改测名称

pub fn add(left: usize, right: usize) -> usize {left + right}#[cfg(test)]mod tests {use super::*;#[test]// 这里修改了测试名称fn exploration() {let result = add(2, 2);assert_eq!(result, 4);}}

结果

让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。第九章讲到了最简单的造成 panic 的方法:调用panic!宏。

pub fn add(left: usize, right: usize) -> usize {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运行测试。它表明exploration测试通过了而another失败了

test tests::another这一行是FAILED而不是ok了。在单独测试结果和摘要之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,another因为在src/lib.rs的第 10 行panicked at 'Make this test fail'而失败。下一部分列出了所有失败的测试,这在有很多测试和很多失败测试的详细输出时很有帮助。

最后是摘要行:总体上讲,测试结果是FAILED。有一个测试通过和一个测试失败。

使用assert!宏来检查结果

assert!宏由标准库提供,在希望确保测试中一些条件为true时非常有用。需要向assert!宏提供一个求值为布尔值的参数。如果值是trueassert!什么也不做,同时测试会通过。如果值为falseassert!调用panic!宏,这会导致测试失败。assert!宏帮助我们检查代码是否以期望的方式运行。

// 结构体struct Rectangle {width: u32,height: u32,}// 结构体实现了can_hold方法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));}}

注意在tests模块中新增加了一行:use super::*;

我们将测试命名为larger_can_hold_smaller,并创建所需的两个Rectangle实例。接着调用assert!宏并传递larger.can_hold(&smaller)调用的结果作为参数。这个表达式预期会返回true,所以测试应该通过。

结果

再来增加另一个测试,这一回断言一个更小的矩形不能放下一个更大的矩形:

fn main() {}#[cfg(test)]mod tests {use super::*;#[test]fn larger_can_hold_smaller() {// --snip--}#[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));}}

也通过了

如果引入一个 bug 的话测试结果会发生什么。将can_hold方法中比较长度时本应使用大于号的地方改成小于号:

impl Rectangle {fn can_hold(&self, other: &Rectangle) -> bool {self.width  other.height}}

结果

我们的测试捕获了 bug!因为larger.length是 8 而smaller.length是 5,can_hold中的长度比较现在因为 8 不小于 5 而返回false

使用assert_eq!和assert_ne!宏来测试相等

测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向assert!宏传递一个使用==运算符的表达式来做到。不过这个操作实在是太常见了,以至于标准库提供了一对宏来更方便的处理这些操作 ——assert_eq!assert_ne!。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试为什么失败,而assert!只会打印出它从==表达式中得到了false值,而不是导致false的两个值。

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));}}

传递给assert_eq!宏的第一个参数4,等于调用add_two(2)的结果。测试中的这一行test tests::it_adds_two ... okok表明测试通过!

在代码中引入一个 bug 来看看使用assert_eq!的测试失败是什么样的。

pub fn add_two(a: i32) -> i32 {a + 3// 这里修改了}#[cfg(test)]mod tests {use super::*;#[test]fn it_adds_two() {assert_eq!(4, add_two(2));}}

结果

测试捕获到了 bug!it_adds_two测试失败,显示信息assertion failed: `(left == right)`并表明left4right5。这个信息有助于我们开始调试:它说assert_eq!left参数是4,而right参数,也就是add_two(2)的结果,是5

需要注意的是,在一些语言和测试框架中,断言两个值相等的函数的参数叫做expectedactual,而且指定参数的顺序是很关键的。然而在 Rust 中,他们则叫做leftright,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成assert_eq!(add_two(2), 4),这时失败信息会变成assertion failed: `(left == right)`其中left5right4

assert_ne!宏在传递给它的两个值不相等时通过,而在相等时失败。

自定义失败信息

也可以向assert!assert_eq!assert_ne!宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来。任何在assert!的一个必需参数和assert_eq!assert_ne!的两个必需参数之后指定的参数都会传递给format!宏,所以可以传递一个包含{}占位符的格式字符串和需要放入占位符的值。自定义信息有助于记录断言的意义;当测试失败时就能更好的理解代码出了什么问题。

例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中:

pub fn greeting(name: &str) -> String {format!("Hello {}!", name)}#[cfg(test)]mod tests {use super::*;#[test]fn greeting_contains_name() {let result = greeting("Carol");assert!(result.contains("Carol"));}}

结果

这个程序的需求还没有被确定,因此问候文本开头的Hello文本很可能会改变。然而我们并不想在需求改变时不得不更新测试,所以相比检查greeting函数返回的确切值,我们将仅仅断言输出的文本中包含输入参数。

让我们通过将greeting改为不包含name来在代码中引入一个 bug 来测试失败时是怎样的:

pub fn greeting(name: &str) -> String {String::from("Hello!")}

结果

结果仅仅告诉了我们断言失败了和失败的行号。一个更有用的失败信息应该打印出greeting函数的值。让我们为测试函数增加一个自定义失败信息参数:带占位符的格式字符串,以及greeting函数的值:

#[test]fn greeting_contains_name() {let result = greeting("Carol");assert!(result.contains("Carol"),"Greeting did not contain name, value was `{}`", result);}

结果

使用should_panic检查panic

除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误也是很重要的。

可以通过对函数增加另一个属性should_panic来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。

pub struct Guess {value: i32,}impl Guess {pub fn new(value: i32) -> Guess {if 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);}}

结果

看起来不错!现在在代码中引入 bug,移除new函数在值大于 100 时会 panic 的条件:

fn main() {}pub struct Guess {value: i32,}// --snip--impl Guess {pub fn new(value: i32) -> Guess {if value < 1{panic!("Guess value must be between 1 and 100, got {}.", value);}Guess {value}}}

结果

这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了#[should_panic]。这个错误意味着代码中测试函数Guess::new(200)并没有产生 panic。

将Result用于测试

也可以使用Result编写测试!这里是第一个例子采用了 Result:

#![allow(unused_variables)]fn main() {#[cfg(test)]mod tests {#[test]fn it_works() -> Result {if 2 + 2 == 4 {Ok(())} else {Err(String::from("two plus two does not equal four"))}}}}

现在it_works函数的返回值类型为Result。在函数体中,不同于调用assert_eq!宏,而是在测试通过时返回Ok(()),在测试失败时返回带有StringErr

这样编写测试来返回Result就可以在函数体中使用问号运算符,如此可以方便的编写任何运算符会返回Err成员的测试。

不能对这些使用Result的测试使用#[should_panic]注解。相反应该在测试失败时直接返回Err值。

11.2运行测试

11.3测试的组织结构

用到再学

参考:测试 – Rust 程序设计语言 简体中文版 (bootcss.com)