🎃个人专栏:
🐬 算法设计与分析:算法设计与分析_IT闫的博客-CSDN博客
🐳Java基础:Java基础_IT闫的博客-CSDN博客
🐋c语言:c语言_IT闫的博客-CSDN博客
🐟MySQL:数据结构_IT闫的博客-CSDN博客
🐠数据结构:数据结构_IT闫的博客-CSDN博客
💎C++:C++_IT闫的博客-CSDN博客
🥽C51单片机:C51单片机(STC89C516)_IT闫的博客-CSDN博客
💻基于HTML5的网页设计及应用:基于HTML5的网页设计及应用_IT闫的博客-CSDN博客
🥏python:python_IT闫的博客-CSDN博客
🐠离散数学:离散数学_IT闫的博客-CSDN博客
🥽Linux:Linux_Y小夜的博客-CSDN博客
🚝Rust:Rust_Y小夜的博客-CSDN博客
欢迎收看,希望对大家有用!
目录
🎯编写和运行测试
🎃测试(函数)
🎃解剖测试函数
🎯 断言(Assert)
🎃使用assert!宏检查测试结果
🎃使用assert_eq!和assert_ne!测试相等性
🎯自定义错误信息
🎯使用should_panic检查恐慌
🎃让should_panic更加精准
🎯在测试中使用Result,e>
Rust 中的测试函数是用来验证非测试代码是否是按照期望的方式运行的。测试函数体通常执行如下三种操作:
- 设置任何所需的数据或状态
- 运行需要测试的代码
- 断言其结果是我们所期望的
测试成功:
Rust 中的测试就是一个带有 test 属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据;第五章中结构体中用到的 derive 属性就是一个例子。为了将一个函数变成测试函数,需要在 fn 行之前加上 #[test]。当使用 cargo test 命令运行测试时,Rust 会构建一个测试执行程序用来调用被标注的函数,并报告每一个测试是通过还是失败。
每次使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这个模块提供了一个编写测试的模板,为此每次开始新项目时不必去查找测试函数的具体结构和语法了。因为这样当然你也可以额外增加任意多的测试函数以及测试模块。
实际编写测试代码之前,让我们先通过尝试那些自动生成的测试模版来探索测试是如何工作的。接着,我们会写一些真正的测试,调用我们编写的代码并断言它们的行为的正确性。
$ cargo new adder --lib Created library `adder` project $ cd adder#[cfg(test)] mod tests { #[test] fn it_works() { let result = 2 + 2; assert_eq!(result, 4); } }现在让我们暂时忽略 tests 模块和 #[cfg(test)] 注解并只关注函数本身。注意 fn 行之前的 #[test]:这个属性表明这是一个测试函数,这样测试执行者就知道将其作为测试处理。tests 模块中也可以有非测试的函数来帮助我们建立通用场景或进行常见操作,必须每次都标明哪些函数是测试。
测试失败:
当测试函数中出现 panic 时测试就失败了。每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。
#[cfg(test)] mod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); } #[test] fn another() { panic!("Make this test fail"); } }
assert! 宏由标准库提供,在希望确保测试中一些条件为 true 时非常有用。需要向 assert! 宏提供一个求值为布尔值的参数。如果值是 true,assert! 什么也不做,同时测试会通过。如果值为 false,assert! 调用 panic! 宏,这会导致测试失败。assert! 宏帮助我们检查代码是否以期望的方式运行。
#[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 } }
测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向 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! 的测试失败是什么样的。修改 add_two 函数的实现使其加 3:
pub fn add_two(a: i32) -> i32 { a + 3 }$ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::it_adds_two ... FAILED failures: ---- tests::it_adds_two stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `4`, right: `5`', src/lib.rs:11:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::it_adds_two test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib`测试捕获到了 bug!it_adds_two 测试失败,错误信息告诉我们断言失败了,它告诉我们 assertion failed: `(left == right)` 以及 left 和 right 的值是什么。这个错误信息有助于我们开始调试:它说 assert_eq! 的 left 参数是 4,而 right 参数,也就是 add_two(2) 的结果,是 5。可以想象当有很多测试在运行时这些信息是多么的有用。
需要注意的是,在一些语言和测试框架中,断言两个值相等的函数的参数被称为 expected 和 actual,而且指定参数的顺序非常重要。然而在 Rust 中,它们则叫做 left 和 right,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成 assert_eq!(add_two(2), 4),这时失败信息仍同样是 assertion failed: `(left == right)`。
assert_ne! 宏在传递给它的两个值不相等时通过,而在相等时失败。在代码按预期运行,我们不确定值 会 是什么,不过能确定值绝对 不会 是什么的时候,这个宏最有用处。
assert_eq! 和 assert_ne! 宏在底层分别使用了 == 和 !=。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必须实现了 PartialEq 和 Debug trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 PartialEq 才能断言它们的值是否相等。需要实现 Debug 才能在断言失败时打印它们的值。因为这两个 trait 都是派生 trait。
也可以向 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 时会通过,而在其中的代码没有 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 测试结果可能会非常含糊不清。should_panic 甚至在一些不是我们期望的原因而导致 panic 时也会通过。为了使 should_panic 测试结果更精确,我们可以给 should_panic 属性增加一个可选的 expected 参数。测试工具会确保错误信息中包含其提供的文本。
// --snip-- 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 属性中 expected 参数提供的值是 Guess::new 函数 panic 信息的子串。我们可以指定期望的整个 panic 信息,在这个例子中是 Guess value must be less than or equal to 100, got 200. 。 expected 信息的选择取决于 panic 信息有多独特或动态,和你希望测试有多准确。在这个例子中,错误信息的子字符串足以确保函数在 else if value > 100 的情况下运行。
目前为止,我们编写的测试在失败时都会 panic。我们也可以使用 Result
编写测试!这是一个延伸自示例 11-1 的测试,使用 Result 重写,并在失败时返回 Err 而非 panic: #[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
的测试使用 #[should_panic] 注解。为了断言一个操作返回 Err 成员,不要使用对 Result 值使用问号表达式(?)。而是使用 assert!(value.is_err())。