Rust 学习笔记:编写自动化测试
Rust 学习笔记:编写自动化测试
Rust 在设计时高度关注程序的正确性,但正确性是复杂的,而且不容易证明。Rust 的类型系统承担了很大一部分的负担,但是类型系统不能捕获所有的东西。因此,Rust 包括对编写自动化软件测试的支持。
本文中,我们将讨论在编写测试时可用的注释和宏。
测试是 Rust 函数,用于验证非测试代码是否按预期方式运行。
测试函数的主体通常执行以下三个动作:
- 设置任何所需的数据或状态。
- 运行要测试的代码。
- 断言结果是你所期望的。
让我们看一下 Rust 为编写执行这些操作的测试提供的特性,其中包括 test 属性、一些宏和 should_panic 属性。
解剖测试函数
简单地说,Rust 中的测试是一个带有test属性注释的函数。
属性是关于 Rust 代码片段的元数据。要将函数更改为测试函数,请在 fn 之前添加 #[test]。
当使用 cargo test 命令运行测试时,Rust 会构建一个 test runner 二进制文件,该文件运行带 #[test] 的函数,并报告每个测试函数是通过还是失败。
每当我们用 Cargo 创建一个新的库项目时,系统都会自动为我们生成一个包含测试函数的测试模块,该模块还提供了编写测试的模板。
让我们创建一个名为 adder 的新库项目,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);
}
}
#[test] 注释表明这是一个测试函数,函数使用 assert_eq! 宏来断言该结果。
cargo test 命令运行项目中的所有测试,输出如下:
Cargo 编译并运行测试。我们看到运行 1 test 的行。下一行显示了生成的测试函数的名称,称为 tests::it_works,并且运行该测试的结果是正常的。总体总结测试结果:ok。表示所有测试都通过了。
从 Doc-tests adder 开始的测试输出的下一部分用于任何文档测试的结果。我们还没有任何文档测试,但是 Rust 可以编译 API 文档中出现的任何代码示例。此功能有助于保持文档和代码同步。
让我们开始根据自己的需要定制测试。修改代码:
- 将 it_works 函数改名为 exploration。
- 新增一个测试函数 another,调用 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,结果如下:
行 test tests::another 显示 FAILED,而不是 ok。在单个结果和总结之间出现两个新部分:第一部分显示每个测试失败的详细原因。第二部分仅列出了所有失败测试的名称。
总结行显示在最后:我们的测试结果是 FAILED。我们有 1 个测试通过,1 个测试失败。
使用 assert! 宏检查结果
assert! 宏由标准库提供,帮助我们检查代码是否按预期的方式运行。
计算结果为布尔值的参数。如果该值为 true,则不发生任何事情,测试通过。如果值为 false,则制造 panic 导致测试失败。
示例:
#[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]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
因为测试模块是一个内部模块,所以我们需要将外部模块中被测试的代码放入内部模块的作用域中。我们在这里使用了 use super::*;
,所以我们在外部模块中定义的任何东西都可以用于这个测试模块。
我们将测试命名为 larger_can_hold_smaller,并创建了我们需要的两个 Rectangle 实例。然后我们调用 assert! 宏并将调用 larger.can_hold(&smaller) 的结果传递给它。这个表达式应该返回 true,所以我们的测试应该通过。用类似的方法创建另一个 smaller_cannot_hold_larger 测试。
运行 Cargo test,两个测试都通过了:
使用 assert_eq! 和 assert_ne! 宏测试相等性
验证功能的一种常用方法是测试被测代码的结果与期望代码返回的值是否相等。可以使用 assert! 宏,并使用 == 操作符将表达式传递给它。
标准库提供了 assert_eq! 和 assert_ne! 宏,可以更方便地执行此测试。这些宏分别比较相等或不相等的两个参数。如果断言失败,它们也会打印这两个值,这使得更容易看到测试失败的原因。
示例:
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, 5);
}
}
2 + 2 当然不等于 5,测试不能通过。
消息告诉我们断言 ‘left == right’ 失败了,以及左值和右值是什么。
如果我们给它的两个值不相等,assert_ne! 宏将通过,如果它们相等,则失败。这个宏在我们不确定一个值应该是什么,但我们知道这个值绝对不应该是什么的情况下最有用。例如,如果我们正在测试一个函数,它保证以某种方式改变其输入,但输入的改变方式取决于我们运行测试的星期几,那么最好断言的可能是函数的输出不等于输入。
在表面之下,assert_eq! 和 assert_ne! 宏分别使用操作符 == 和 !=。当断言失败时,这些宏使用调试格式打印它们的参数,这意味着要比较的值必须实现 PartialEq 和 Debug trait。所有基本类型和大多数标准库类型都实现了这些特征。对于自己定义的结构体和枚举,需要实现 PartialEq 来断言这些类型的相等性。还需要实现 Debug,以便在断言失败时打印值。因为这两个 trait 都是可衍生的 trait,所以添加 #[derived (PartialEq, Debug)] 注释即可。
添加自定义失败消息
assert!、assert_eq! 和 assert_ne! 宏都支持将失败消息作为其可选参数打印。
在必需参数之后指定的任何参数都将传递给 format! 宏,所以可以传递一个格式字符串,其中包含 {} 占位符和那些占位符中的值。
自定义消息对于记录断言的含义非常有用。当测试失败时,可以更好地了解代码的问题所在。
编写一个出错的示例:
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{result}`"
);
}
}
当我们运行测试时,我们可以看到我们在测试输出中实际得到的值,这将对调试很有帮助。
使用 should_panic 检查 panic
除了检查返回值之外,检查代码是否按预期处理错误条件也很重要。例如,考虑我们在猜数游戏中创建的 Guess 类型。使用 Guess 的其他代码依赖于 Guess 实例只包含 1 到 100 之间的值的保证。我们可以编写一个测试,以确保尝试创建具有超出该范围的值的Guess实例时会发生恐慌。
为此,我们将属性 should_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] 属性之后、测试函数之前。
这个测试函数的主体内的代码会导致 panic,因此测试通过:
为了使 should_panic 测试更精确,我们可以向 should_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);
}
}
这个测试将通过,因为我们在 should_panic 属性的预期参数中输入的值是 Guess::new 函数处理的消息的子字符串。
为了了解当带有预期消息的should_panic测试失败时会发生什么,让我们通过交换 if 和 else 引入一个错误:
if value < 1 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
);
}
失败消息表明这个测试确实像我们预期的那样出现了 panic,但是 panic 消息没有 except 参数中预期的 “less than or equal to 100” 字符串。在这种情况下,我们得到的恐慌信息是 “Guess value must be greater than or equal to 1, got 200.”。现在我们可以开始找出 bug 在哪里了!
在测试中使用 Result<T, E>
到目前为止,我们的测试函数失败时都会 panic。我们还可以使用 Result<T, E>,在测试失败时返回 Err。
#[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"))
}
}
it_works 函数现在具有 Result<(), String> 返回类型。当测试通过时返回 Ok(()),当测试失败时返回 Err,其中包含一个 String。
编写测试使它们返回 Result<T, E> 使得能够在测试体中使用 ? 操作符,这是编写测试的一种方便的方法,如果其中的任何操作返回 Err 变体,则 panic,测试失败。
但是,断言某操作返回 Err,不要使用 ? 操作符,而应该使用 assert!(value.is_err())。
不能在使用 Result<T, E> 的测试函数上使用 #[should_panic] 注释。