一、如何编写测试
测试函数的函数体中一般包含3个部分:
- 准备所需的数据或状态;
- 调用需要测试的代码;
- 断言运行结果与我们所期望的一致;
1、测试函数的构成
在最简单的情形下,Rust中的测试说就是一个标注有test属性的函数。属性(attribute)是一种用于修饰Rust代码的元数据。
只需要将#[test]
添加到关键字fn
的上一行便可将函数转变为测试函数。
当测试函数编写完成后,可以使用cargo test
命令来运行测试。这个命令会构建并执行一个用于测试的可执行文件,该文件在执行的过程中会逐一调用所有标注test
属性的函数,并生成统计测试运行成功或失败的相关报告。
当我们使用cargo
新建一个库项目时,它会自动为我们创建一个带有测试函数的测试模块。
cargo new adder --lib //新建adder的库项目
在这个库项目下会自动生成src/lib.rs
文件,并显示如下内容:
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);
}
}
先忽略最上面八行代码。#[test]
标注:它将当前的函数标记为一个测试,并使该函数可以在测试运行的过程中被识别出来。
Cargo成功编译并运行了这段代码后,输出了running 1 test
,表示当前正在执行1个测试,接下来显式的所生成的测试函数名称it_works
,以及相应的测试结果。
test result: ok.
:表示该结合中的所有测试均成功通过;1 passed
:成功的测试总数;0 failed
:失败的测试总数;0 ignored
:忽略的测试总数;0 measured
:测量性能的测试数量;0 filtered out
:过滤运行的测试数量;finished in 0.00s
:完成时间;
Doc-tests adder
开头的部分表示文档测试的结果。
我们通过panic!
宏故意导致程序运行失败。因为在Rust中,一旦测试函数触发panic,该测试就会被视作执行失败。
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);
}
#[test]
fn another() {
panic!("Make this test fail");
}
}
再次运行cargo test
运行测试,输出如下结果:
2、使用assert!
宏检查结果
assert!
宏由标准库提供,它可以确保测试中某些条件的值为true
。assert!
宏可以接收一个能够被计算为布尔类型的值作为参数。当这个值为true
时,assert!
宏什么都不用做并正常通过测试,而当值为false
时,assert!
宏就会调用panic!
宏,进而导致测试失败。
3、使用assert_eq!
宏和assert_ne!
宏判断相等性
assert_eq!
和assert_ne!
这两个宏分别用于比较并断言两个参数相等或不相等。在断言失败时,他们还可以自动打印出两个参数的值,从而方便我们观察测试失败的原因;相反,使用assert!
宏则只能得知==
判断表达式失败的事实,而无法知晓被用于比较的值。
从本质上看,assert_eq!
和assert_ne!
宏分别使用了==
和!=
运算符来进行判断,并在断言失败时使用调试输出格式({:?}
)将参数值打印出来,这意味着它们的参数必须同时实现PartialEq
和Debug
这两个trait。
当我们自定义的结构体和枚举时,需要自动实现PartialEq
来判断两个值是否相等,并实现Debug
来保证值可以在断言失败时被打印出来。由于这两个trait都是可派生trait,所以它们一般可以通过自定义的结构体或枚举定义的上方添加#[derive(PartialEq, Debug)]
标注来自动实现这两个traiit。
4、添加自定义的错误提示信息
实际上,任何在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
)
}
}
这次的测试输出了中包含了实际的值,它能帮助我们观察程序真正发生的行为,并循序定位与预期产生差异的地方。
5、使用should_panic
检查panic
should_panic
属性:这个属性的测试函数会在代码发生panic时顺利通过,而在代码不发生panic时执行失败。
pub struct Guess {
value: u32,
}
impl Guess {
pub fn new(value: u32) -> 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]
属性之后、对应的测试函数之前。测试顺利通过。
我们在代码中引入bug,删除new函数中值大于100时发生panic的判断条件:
impl Guess {
pub fn new(value: u32) -> Guess {
if value < 1 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess {
value
}
}
}
再次运行测试,则会输出测试失败的结果:
为了让should_panic
测试更加精确一些,我们可以在should_panic
属性中添加可选参数expected
,它会检查panic发生时输出的错误提示新是否包含了指定文字。
pub struct Guess {
value: i32,
}
// --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 = "Guess value must be less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
因为Guess::new
函数在发生panic的输出消息中包含了should_panic
属性的expected
参数指定的文本,所以该示例中的测试会顺利通过。一般来说,expected参数中的内容取决于panic信息是明确的还是易变,也取决于测试本身需要准确到何种程度。
6、使用Result<T, E>编写测试
#[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值。
二、控制测试的运行方式
1、并行或串行地进行测试
当你尝试运行多个测试时,Rust会默认使用多线程来并行执行它们。如果你不想并行运行测试,或希望精确地控制测试时所启动地线程数量,那么可通过给测试二进制文件传入--test=threads
标记及期望地具体线程数量来控制这一行为。
cargo test -- --test-threads=1
2、显式函数输出
默认情况下,Rust的测试库会在测试通过是捕获所有被打印值标准输出中的消息。例如,我们在测试中调用了println!
,但hi要测试顺利通过,它所打印的内容就无法显式在 终端上。
如果你希望在测试通过时也将值打印出来,那么可以传入一个nocapture
标记来禁用输出截获功能:
cargo test -- --nocapture
3、只运行部分特定名称的测试
执行全部的测试用例有时比较耗费时间,我们可以通过cargo test
中传递测试名称来指定需要运行的测试。
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
assert_eq!(4, add_two(2));
}
#[test]
fn add_three_and_two() {
assert_eq!(5, add_two(3));
}
#[test]
fn one_hundred() {
assert_eq!(102, add_two(100));
}
}
运行单个测试:给cargo test
传递一个测试函数的名称来单独运行测试。
cargo test one_hundred
只有名为one_hundred
的测试得到了运行,因为其余两个测试的名称无法匹配我们传入的参数。同时,测试输出还通过摘要信息一行中的2 filtered out
表明部分测试被过滤了。
需要注意的是,我们不能指定多个参数来运行多个测试:只有传递给cargo test
的第一个参数才会生效。
通过过滤名称来运行多个测试:
cargo test add
这个命令运行了所有名称中带有add的测试,并将名为one_hundred
的测试过滤。
4、通过显式指定来忽略某些测试
除了手动将想要运行的测试列举出来,你还可以使用ignore
属性来标记耗时的测试,并将这些测试排除在正常的测试运行之外。
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// 需要运行一个小时的代码
}
expensive test
函数被放到了ignored
类别下,我们可以使用cargp test -- --ignored
来单独运行这些被忽略的测试。
通过控制测试的运行,我们可以保证每次执行cargo test都能迅速得到结果。
三、测试的组织结构
Rust社区主要从以下两种分类来讨论测试:单元测试(unit test)和集成测试(integration test)。单元测试小而专注,每次只单独测试一个模块或私有结构;而集成测试完全位于代码库之外,和正常从外部嗲用代码库一样使用外部代码,只能访问公共结构,并且在一次测试中可能会联用多个模块。
标签:10,value,assert,测试,自动化,test,编写,panic,fn From: https://www.cnblogs.com/ColoFly/p/17010555.html