编写与运行测试
测试(函数)
- 测试的目的是为了验证非测试代码的功能是否与预期一致。
- 测试函数体通常执行 3 个操作:
- 测试数据/状态。
- 运行被测试的代码。
- 断言结果。
测试函数结构
- 测试函数需要使用 test 属性(attribute)进行标注:
- Attribute 就是一段 Rust 代码的元数据,不会改变已有代码的逻辑。
- 在函数上加
#[test]
,可把函数变成测试函数。
运行测试
使用
cargo test
命令运行所有测试函数。- Rust 会构建一个 Test Runner 可执行文件。
- 它会运行标注了 test 的函数,并报告其是否运行成功。
- Rust 会构建一个 Test Runner 可执行文件。
当使用 cargo 创建 library 项目时,会生成一个 test module,包含来一个 test 函数。
- 可以添加任意数量的 test module 或函数。
#[cfg(test)] mod tests { #[test] fn it_works() { let result = 2 + 2; assert_eq!(result, 4); } }
测试失败
测试函数 panic 就表示失败。
每个测试运行在一个新线程。
当主线程看见某个测试线程挂掉来,那个测试就标记为失败。
#[test] fn another() { panic!("Make this test fail!!!") }
还可以使用 assert! 宏来检查测试结果,其来自标准库,用来确定某个状态是否为 true。
- true 表示通过。
- false 表示失败。
#[derive(Debug)] pub struct Rectangle { length: u32, width: u32, } impl Rectangle { pub fn can_hold(&self, other: &Rectangle) -> bool { self.length > other.length && self.width > other.width } } #[cfg(test)] mod tests { use super::*; #[test] fn larger_can_hold_smaller() { let larger = Rectangle { length: 8, width: 7, }; let smaller = Rectangle { length: 5, width: 1, }; assert!(larger.can_hold(&smaller)); } }
使用
assert_eq!
和assert_ne!
测试相等性:都来自标准库。
判断两个参数是否
相等
或不等
。实际上,它们使用的就是
==
和!=
运算符。特点是当断言失败时,这两个宏可以打印参数当值。
使用 debug 格式打印参数:要求参数实现了
PartialEq
和Debug Traits
(所有当基本类型和标准库里大部分类型都实现了)。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!
、assert_eq!
、assert_ne!
添加可选的自定义消息:- 这些自定义消息和失败消息都会打印出来。
assert!
:第一个参数必填,自定义消息作为第二个参数。assert_eq!
、assert_ne!
:前两个参数必填,自定义消息作为第三个参数。- 自定义消息参数会被传递给
format!
宏,可以使用{}
占位符。
/* running 1 test thread 'tests::greetings_contain_name' panicked at 'Greeting didn't contain name, value was 'Hello Carl!'', src/ lib.rs:12:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace test tests::greetings_contain_name ... FAILED */ pub fn greeting(name: &str) -> String { format!("Hello {}!", name) } #[cfg(test)] mod tests { use super::*; #[test] fn greetings_contain_name() { //let result = greeting("Carol"); let result = greeting("Carl"); assert!( result.contains("Carol"), "Greeting didn't contain name, value was '{}'", result ); } }
检查 panic
验证错误处理的情况
测试除了验证代码的返回值是否正确,还需要验证代码是否如预期的处理了发生错误的情况。
可验证代码在特定情况下是否发生了 panic。
should_panic
属性(attribute):- 函数 panic:测试通过。
- 函数没有发生 panic:测试失败。
/* running 1 test test tests::greater_than_100 - should panic ... FAILED failures: ---- tests::greater_than_100 stdout ---- note: test did not panic as expected failures: tests::greater_than_100 */ pub struct Guess { value: u32, } impl Guess { pub fn new(value: u32) -> Guess { if !(1..=100).contains(&value) { 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(10); } }
为
should_panic
属性添加一个可选的 expected 参数:- 将检查失败消息中是否包含指定的文字,使得测试目标更加精确。
/* 尽管都发生了 panic 但是由于检查的匹配字符串不同依旧会测试失败 ---- tests::greater_than_100 stdout ---- note: panic did not contain expected string panic message: `"Guess value must be greater than or equal 1, got 0"`, expected substring: `"Guess value must be less than or equal 100"` failures: tests::greater_than_100 */ pub struct Guess { value: u32, } impl Guess { pub fn new(value: u32) -> Guess { if value < 1 { panic!("Guess value must be greater than or equal 1, got {}", value) } else if value > 100 { panic!("Guess value must be less than or equal 100, got {}", value) } Guess { value } } } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic(expected = "Guess value must be less than or equal 100")] fn greater_than_100() { Guess::new(0); } }
在测试中使用 Result<T, E>
无需 panic,可使用 Result<T, E> 作为返回类型编写测试:
- 返回 OK:测试通过。
- 返回 Err:测试失败。
- 注意:不要在使用 Result 编写的测试用例上标注
#[should_panic]
。
/* Error: "two plus two does not euqal four" thread 'tests::it_works' 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', / rustc/7737e0b5c4103216d6fd8cf941b7ab9bdbaace7c/library/test/src/lib.rs:187:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace test tests::it_works ... FAILED */ #[cfg(test)] mod tests { #[test] fn it_works() -> Result<(), String> { if 2 + 3 == 4 { Ok(()) } else { Err(String::from("two plus two does not euqal four")) } } }
控制测试如何运行
- 通过添加命令行参数可以改变
cargo test
命令的行为。- 默认行为:
- 并行运行。
- 执行所有测试。
- 捕获(不显示)所有输出(在测试成功的情况下),使读取与测试结果相关的输出更容易。
- 默认行为:
- 命令行参数:
- 针对
cargo test
的参数:紧跟cargo test
后。 - 针对测试可执行程序:放在
--
之后,例如cargo test --help
。 cargo test -- --help
可以显示出所有可以应用于--
之后的参数。
- 针对
并行/串行(连续)运行测试
并行运行:默认使用多个线程并行运行:
- 优势:运行速度块。
- 但是需要确保测试之间:
- 不会相互依赖。
- 不依赖于某个共享状态(环境、工作目录、环境变量等等)。
如果不想并行运行,或控制并行的线程数量使用:
--test-threads
参数。- 传递给二进制的参数。
- 可以控制测试程序的线程数量。
- 例如:
cargo test -- --test-threads=1
。
显式函数输出
- 默认测试程序对于测试成功的函数会
捕获即不显示
输出,只有当测试失败时才会打印出来。- 使用
--show_output
可以控制测试程序显示成功的输出。
- 使用
通过测试名称运行测试
选择运行的测试:将测试的名称(一个或多个)作为 cargo test 的参数。
- 运行单个测试:指定测试名称,例如
cargo test test_func_name
。 - 运行多个测试:指定测试名称的一部分(模块名也可以),例如
cargo test it
,其中it
是两个测试名称都包含的部分。
/* running 2 tests test tests::it_works ... ok test tests::it_not_work ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s */ #[cfg(test)] mod tests { #[test] fn it_works() -> Result<(), String> { if 2 + 2 == 4 { Ok(()) } else { Err(String::from("two plus two does not euqal four")) } } #[test] fn it_not_work() -> Result<(), String> { if 3 + 3 == 6 { Ok(()) } else { Err(String::from("two plus two does not euqal four")) } } }
- 运行单个测试:指定测试名称,例如
忽略某些测试
ignore
属性(Attribute)。想要运行被忽略的程序可以使用
--ignored
参数,例如cargo test -- --ignored
#[cfg(test)] mod tests { #[test] #[ignore = "long time"] fn it_works() -> Result<(), String> { if 2 + 2 == 4 { Ok(()) } else { Err(String::from("two plus two does not euqal four")) } } #[test] fn it_not_work() -> Result<(), String> { if 3 + 3 == 6 { Ok(()) } else { Err(String::from("two plus two does not euqal four")) } } }
测试的组织
测试的分类
- Rust 对测试的分类:
- 单元测试。
- 集成测试。
- 单元测试:
- 小、专注。
- 一次对一个模块进行隔离测试。
- 可测试 private 接口。
- 集成测试:
- 在库外部。和其他外部代码一样使用你的代码。
- 只能使用 public 接口。
- 可能在每个测试中使用到多个模块。
单元测试
- 单元测试需要使用
#[cfg(test)]
标注。- 只用运行 cargo test 才会编译和运行代码。
- 运行 cargo build 则不会。
- 集成测试在不同的目录,它不需要
#[cfg(test)]
标注。 - cfg:configuration(配置):
- 告诉 Rust 下面的条目只有在指定的配置选项下才被包含。
- 配置选项 test:由 Rust 提供,用来编译和运行测试。
- 只用 cargo test 才会编译代码,包括模块中的 helper 函数和 #[test] 标注的函数。
测试私用函数
Rust 允许测试私有函数。
pub fn add_two(a: i32) -> i32 { internal_adder(a, 2) } fn internal_adder(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { assert_eq!(4, internal_adder(2, 2)) } }
集成测试
- 在 Rust 中,集成测试完全位于被测试库的外部。
- 目的:是测试被测试库的多个部分是否能正确的一起工作。
- 集成测是覆盖率很重要。
tests 目录
- 创建集成测试:tests 目录。
- tests 目录下的每一个测试文件都是单独的一个 crate。
- 需要将被测试的库导入。
- 无需标注 #[cfg(test)],tests 目录被特殊对待。
- tests 目录下的每一个测试文件都是单独的一个 crate。
/*
.
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
*/
use adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
运行指定的集成测试
运行一个特定的集成测试:cargo test 函数名称。
运行某个测试文件内的所有测试:cargo test –test 文件名。
tests 目录下的每个文件被编译成单独的 crate:
- 这些文件不共享行为(于 src 目录下规则不同)。
- 如下例,本意的 common 只是工具方法,却別认为是一个单独的测试用例。
Running tests/common.rs (target/debug/deps/common-36a0cc1efb478906) running 0 tests . ├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── tests ├── common.rs └── integration_test.rs
- 如下创建,mod 目录来存放工具方法,common 下不再被是被为单独的 crate:
. ├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── tests ├── common │ └── mod.rs └── integration_test.rs
针对 binary crate 的集成测试
- 如果项目是 binary crate,只含有 src/main.rs 没有 src/lib.rs:
- 不能在 tests 目录下创建集成测试。
- 无法把 main.rs 的函数导入作用域。
- 只用 library crate 才能暴露函数给其他 crate 用。
- binary crate 意味着独立运行。