测试
[TOC]
1. 编写测试的基本操作
Rust中使用函数进行代码的测试,用以验证非测试代码是否按照期望的方式运行,一般具有以下操作:
设置所需要的数据或者状态
运行测试代码
断言结果是否为所期望的
1.1 test 属性的注释函数
Rust 中测试函数是一个带有test
属性注释的函数。属性attribute是关于Rust代码片段的元数据,derive
就是一个典型的例子。为了将一个函数设置为测试函数,可以在函数名前加上一个#[test]
.当执行cargo test
时,Rust会构建一个测试程序来调用所有标记了该属性注释的函数。
对于一个成本的测试样例,得到的运行结果:
running 7 tests
test tests::it_works ... ok
test tests::rectangle_large_can_hold_small ... ok
test tests::greeting_not_done ... FAILED
test tests::length_negative ... ok
test tests::rectangle_small_can_not_hold_large ... ok
test tests::return_result ... FAILED
test tests::width_negative ... ok
failures:
---- tests::greeting_not_done stdout ----
thread 'tests::greeting_not_done' panicked at 'Greeting did not contain the name value:Hello, Cargo', src/lib.rs:40:9
note: Run with `RUST_BACKTRACE=1` for a backtrace.
---- tests::return_result stdout ----
Error: "two add two does not equal three"
thread 'tests::return_result' panicked at 'assertion failed: `(left == right)`
left: `1`,
right: `0`: the test returned a termination value with a non-zero status code (1) which indicates a failure', src/libtest/lib.rs:337:5
failures:
tests::greeting_not_done
tests::return_result
test result: FAILED. 5 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out
输出了所有的失败样例,以及测试结果总结,
通常在一个测试函数中,使用assert!
类的宏进行测试的第三项工作,该宏接受一个bool
类型的值,通常使用的宏还有assert_eq!
, assert_ne!
,接受两个类型相同的值,断言相等或者不等。需要注意的是,在一些语言和测试框架中,断言两个值相等的函数的参数叫做 expected
和 actual
,而且指定参数的顺序是很关键的。然而在 Rust 中,他们则叫做 left
和 right
,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成 assert_eq!(add_two(2), 4)
,这时失败信息会变成 assertion failed:
(left == right)` 其中
left是
5而
right是
4`。
这两个宏底层实现使用了== !=
, 同时如果断言失败会使用Debug
模式输出错误信息。所以传入的类型必须是实现了PartialEq
以及Debug
trait 的类型。又因为这两个trait都是派生trait,通常可以在结构体上加#[derive(PartialEq + Debug)]
属性注释。
1.2 自定义失败信息
可以向assert!
等宏中添加一个可选的默认失败信息,在测试失败时将这些信息打印出来。获取更加详细的测试信息。
例如,对于一个尚未完成的函数进行测试,由于可能Hello
可变,只对参数进行测试:
# fn main() {}
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"));
}
}
如果测试失败,显示的结果只有失败的函数以及行号:
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at 'assertion failed:
result.contains("Carol")', src/lib.rs:12:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
tests::greeting_contains_name
可以添加更多信息:
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`", result
);
}
1.3 panic 测试
对于代码中的panic进行测试时,可以使用should_panic
属性注释进行操作:
#[test]
#[should_panic(expected = "The incorrect value of rectangle: 12.1, -5")]
fn width_negative() {
Rectangle::new(12.1, -5.);
}
impl Rectangle {
pub fn new(length: f64, width: f64) -> Rectangle {
if length <= 0. || width <= 0. {
panic!("The incorrect value of rectangle: {}, {}", length, width);
}
Rectangle {
length,
width,
}
}
}
但是使用这种方式有一个很大的问题,如果是程序的其他部分panic那么该测试样例也是可以通过的,是的测试结果十分含糊不清,所以一般需要加上一个expeted
参数,从而获取所期望的panic信息。
1.4 用Result<T, E>
进行测试
Result<T, E>
进行测试可以在测试函数返回一个Result<T, E>
枚举类型,可以达到同样的效果:
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。同时在函数体中,在成功时返回 Ok(())
而不是 assert_eq!
,而失败时返回带有 String
的 Err
。跟之前一样,这个测试可能成功或失败,不过不再通过 panic,可以通过 Result<T, E>
来判断结果。为此不能在对这些函数使用 #[should_panic]
;而是应该返回 Err
!
2. 测试控制
cargo test
命令会生成可执行的二进制文件,默认进行的是并行测试,并且截获所有的输出,阻止其输出到console,从而获得更为清晰地测试结果。可以将一部分命令行参数传递给cargo test
,而另一部分传递给生成的二进制文件,先列出cargo test
的参数,然后是二进制文件参数。
2.1 并行或者串行执行测试
并执行测试时必须保证测试程序间不会生竞争条件,所以测试间不可以相互依赖,或者依赖共享的状态。依赖共享的环境。为了避免测试相互干扰,可以指定单线程运行:
cargo test -- -- test-thread=1
2.2 显示函数输出
cargo test -- -- nocapture
2.3 运行单个测试
cargo test func_name
可以通过指定一个测试函数或者测试模块,运行一部分测试,节省测试时间。这种方式称为filter
2.4 忽略某些测试
对于一些较为耗时的测试,可以添加#[ignore]
属性注释进行忽略,从而加快测试进度。
3. 测试的组织结构
本章一开始就提到,测试是一个复杂的概念,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试(unit tests)与 集成测试(integration tests)。单元测试倾向于更小而更集中,在隔离的环境中一次测试一个模块,或者是测试私有接口。而集成测试对于你的库来说则完全是外部的。它们与其他外部代码一样,通过相同的方式使用你的代码,只测试公有接口而且每个测试都有可能会测试多个模块。
为了保证你的库能够按照你的预期运行,从独立和整体的角度编写这两类测试都是非常重要的。
3.1 单元测试
单元测试的主要目的是在与其他部分隔离的环境中进行代码的测试。以便于快速而准确地确定某个部分的代码是否符合预期。单元测试与需要测试的代码都存放在src
目录下,规范是包含测试函数的tests
模块,需要使用#cfg(test)
属性标注。
通过使用#cfg(test)
保证了测试代码只在cargo test
时才编译运行,在cargo build
时不需要这么做。可以在构建库时节省时间。同时由于单元测试代码和库文件在同一个文件夹中,所以需要这个属性进行标示,指定他们不可以包含在库文件中。
3.2 集成测试
在 Rust 中,集成测试对于所需要测试的库是完全外部的。同其他使用库的代码一样使用库文件,也就是说只可以调用库中的pub
API。集成测试的目的是为了保证多个部分是否可以协同工作。集成测试在tests
文件夹内进行。
tests
目录与scr
目录同级,不需要加#cfg(test)
属性 tests
文件夹在 Cargo 中是一个特殊的文件夹, Cargo 只会在运行 cargo test
时编译这个目录中的文件。
Compiling adder v0.1.0 (/home/inno/Learning-notes/RustInfo/projects/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.23s
Running target/debug/deps/adder-70257eb04d3ac84f
running 7 tests
test tests::greeting_not_done ... ok
test tests::it_works ... ok
test tests::rectangle_large_can_hold_small ... ok
test tests::rectangle_small_can_not_hold_large ... ok
test tests::length_negative ... ok
test tests::return_result ... ok
test tests::width_negative ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/integration_test-b8e9d862b2a5bae4
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
现在有了三个部分的输出:单元测试、集成测试和文档测试。仍然可以通过指定测试函数的名称作为 cargo test
的参数来运行特定集成测试。也可以使用 cargo test
的 --test
后跟文件的名称来运行某个特定集成测试文件中的所有测试:
Last updated
Was this helpful?