?个人专栏:
? 算法设计与分析:算法设计与分析_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)
?使用assert!宏检查测试结果
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_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!
的测试失败是什么样的。修改 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 testtest tests::it_adds_two ... FAILEDfailures:---- tests::it_adds_two stdout ----thread 'main' panicked at 'assertion failed: `(left == right)` left: `4`, right: `5`', src/lib.rs:11:9note: run with `RUST_BACKTRACE=1` environment variable to display a backtracefailures: tests::it_adds_twotest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: 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检查恐慌
除了检查返回值之外,检查代码是否按照期望处理错误也是很重要的。
可以通过对函数增加另一个属性 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
测试结果可能会非常含糊不清。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
的情况下运行。
?在测试中使用Result<T,E>
目前为止,我们编写的测试在失败时都会 panic。我们也可以使用 Result<T, E>
编写测试!这是一个延伸自示例 11-1 的测试,使用 Result<T, E>
重写,并在失败时返回 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<T, E>
的测试使用 #[should_panic]
注解。为了断言一个操作返回 Err
成员,不要使用对 Result<T, E>
值使用问号表达式(?
)。而是使用 assert!(value.is_err())
。