Rust 是一个相当注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有种类的错误。为此,Rust 也在语言本身包含了编写软件测试的支持。
编写一个叫做add_two
的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的类型检查和借用检查,例如,这些检查会确保我们不会传递String
或无效的引用给这个函数。Rust 所不能检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。
可以编写测试断言,比如说,当传递3
给add_two
函数时,返回值是5
。无论何时对代码进行修改,都可以运行测试来确保任何现存的正确行为没有被改变。
11.1编写测试
如何编写测试
Rust 中的测试函数是用来验证非测试代码是否按照期望的方式运行的。测试函数体通常执行如下三种操作:
- 设置任何所需的数据或状态
- 运行需要测试的代码
- 断言其结果是我们所期望的
测试函数剖析
作为最简单例子,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 编译并运行了测试。在Compiling
、Finished
和Running
这几行之后,可以看到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!
宏提供一个求值为布尔值的参数。如果值是true
,assert!
什么也不做,同时测试会通过。如果值为false
,assert!
调用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 ... ok
中ok
表明测试通过!
在代码中引入一个 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)`
并表明left
是4
而right
是5
。这个信息有助于我们开始调试:它说assert_eq!
的left
参数是4
,而right
参数,也就是add_two(2)
的结果,是5
。
需要注意的是,在一些语言和测试框架中,断言两个值相等的函数的参数叫做expected
和actual
,而且指定参数的顺序是很关键的。然而在 Rust 中,他们则叫做left
和right
,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成assert_eq!(add_two(2), 4)
,这时失败信息会变成assertion failed: `(left == right)`
其中left
是5
而right
是4
。
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(())
,在测试失败时返回带有String
的Err
。
这样编写测试来返回Result
就可以在函数体中使用问号运算符,如此可以方便的编写任何运算符会返回Err
成员的测试。
不能对这些使用Result
的测试使用#[should_panic]
注解。相反应该在测试失败时直接返回Err
值。
11.2运行测试
11.3测试的组织结构
用到再学
参考:测试 – Rust 程序设计语言 简体中文版 (bootcss.com)