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.width && self.height > 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 < 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);
}
}
结果
看起来不错!现在在代码中引入 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<T,E>用于测试
也可以使用 Result<T, E>
编写测试!这里是第一个例子采用了 Result:
#![allow(unused_variables)]
fn main() {
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
}
现在 it_works
函数的返回值类型为 Result<(), String>
。在函数体中,不同于调用 assert_eq!
宏,而是在测试通过时返回 Ok(())
,在测试失败时返回带有 String
的 Err
。
这样编写测试来返回 Result<T, E>
就可以在函数体中使用问号运算符,如此可以方便的编写任何运算符会返回 Err
成员的测试。
不能对这些使用 Result<T, E>
的测试使用 #[should_panic]
注解。相反应该在测试失败时直接返回 Err
值。
11.2 运行测试
11.3 测试的组织结构
用到再学
参考: 测试 - Rust 程序设计语言 简体中文版 (bootcss.com)