Rust 中的测试函数是用来验证代码是否是按照你期望的方式运行的一类函数:
函数测试
Rust 中的测试就是一个带有 test
属性注解的函数,当使用 cargo test
命令运行测试时,Rust 会构建一个测试执行程序用来调用标记了 test
属性的函数,并报告每一个测试是通过还是失败。
#[test]
fn it_works() {assert_eq!(2 + 2, 4);
}
函数体通过使用 assert_eq!
宏来断言 2 加 2 等于 4。一个典型的测试的格式,运行后可以看到,显示了生成的测试函数的名称,它是 it_works
,以及测试的运行结果,ok
。接着可以看到全体测试运行结果的摘要:test result: ok.
意味着所有测试都通过了。1 passed; 0 failed
表示通过或失败的测试数量。我们并没有将任何测试标记为忽略,所以摘要中会显示 0 ignored
。我们也没有过滤需要运行的测试,所以摘要中会显示0 filtered out
。0 measured
统计是针对性能测试的
$ cargo testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.57sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 1 test
test tests::it_works ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
当测试函数中出现 panic 时测试就失败了,每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败
#[test]
fn another() {panic!("Make this test fail");
}
test tests::another
这一行是 FAILED
而不是 ok
了,只要有一个函数是 FAILED ,则整个函数的测试结果是 FAILED
$ cargo testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.72sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 2 tests
test tests::another ... FAILED
test tests::exploration ... okfailures:---- tests::another stdout ----
thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtracefailures:tests::anothertest result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass '--lib'
用于测试的API
assert!
宏由标准库提供,在希望确保测试中一些条件为 true
时非常有用,需要向 assert!
宏提供一个求值为布尔值的参数。如果值是 true
,assert!
什么也不做,同时测试会通过。如果值为 false
,assert!
调用 panic!
宏,这会导致测试失败
#[test]
fn test2() {let re;if(2+2 == 4){re = true;}else{re = false}assert!(re);
}
assert_eq!
和 assert_ne!
。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试 为什么失败,而 assert!
只会打印出它从 ==
表达式中得到了 false
值,而不是导致 false
的两个值。
pub fn add_two(a: i32) -> i32 {a + 3
}
#[cfg(test)]
mod tests {use super::*;#[test]fn it_adds_two() {assert_eq!(4, add_two(2));}
}
测试捕获到了 bug!it_adds_two 测试失败,显示信息 assertion failed: (left == right)
并表明 left 是 4 而 right 是 5。这个信息有助于我们开始调试:它说 assert_eq! 的 left 参数是 4,而 right 参数,也就是 add_two(2) 的结果,是 5。
$ cargo testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.61sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 1 test
test 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:9
note: 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'
属性 should_panic
会让函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。#[should_panic]
属性位于 #[test]
之后,对应的测试函数之前
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);}
}
$ cargo testCompiling guessing_game v0.1.0 (file:///projects/guessing_game)Finished test [unoptimized + debuginfo] target(s) in 0.58sRunning unittests (target/debug/deps/guessing_game-57d70c3acb738f4d)running 1 test
test tests::greater_than_100 - should panic ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests guessing_gamerunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
为了使 should_panic
测试结果更精确,我们可以给 should_panic
属性增加一个可选的 expected
参数。测试工具会确保错误信息中包含其提供的文本,如下的这个测试会通过,因为 should_panic
属性中 expected
参数提供的值是 Guess::new
函数 panic 信息的子串
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);}
}
自定义错误信息
你也可以向 assert!
、assert_eq!
和 assert_ne!
宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来,如下的函数:
#[test]
fn greeting_contains_name() {let result = greeting("Carol");assert!(result.contains("Carol"),"Greeting did not contain name, value was `{}`",result);
}}
如果这个函数的运行结果是失败的,将会打印出自定义失败信息参数
$ cargo testCompiling greeter v0.1.0 (file:///projects/greeter)Finished test [unoptimized + debuginfo] target(s) in 0.93sRunning unittests (target/debug/deps/greeter-170b942eb5bf5e3a)running 1 test
test tests::greeting_contains_name ... FAILEDfailures:---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtracefailures:tests::greeting_contains_nametest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass '--lib'
Result测试
我们也可以使用 Result<T, E>
编写测试,使用 Result<T, E>
重写,并在失败时返回 Err
而非 panic,测试通过时返回 Ok(())
,在测试失败时返回带有 String
的 Err
。
#[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"))}}
}
控制测试运行
当运行多个测试时, Rust 默认使用线程来并行运行。这意味着测试会更快地运行完毕,所以你可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该确保测试不能相互依赖,或依赖任何共享的状态,包括依赖共享的环境,比如当前工作目录或者环境变量。
举个例子,每一个测试都运行一些代码,假设这些代码都在硬盘上创建一个 test-output.txt 文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中修改了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干扰。一个解决方案是使每一个测试读写不同的文件;另一个解决方案是一次运行一个测试。
如果你不希望测试并行运行,或者想要更加精确的控制线程的数量,可以传递 --test-threads
参数和希望使用线程的数量给测试二进制文件。
$ cargo test -- --test-threads=1
默认情况下,当测试通过时,Rust 的测试库会截获打印到标准输出的所有内容。比如在测试中调用了 println!
而测试通过了,我们将不会在终端看到 println!
的输出:只会看到说明测试通过的提示行。如果测试失败了,则会看到所有标准输出和其他错误信息。
如果你希望也能看到通过的测试中打印的值,也可以在结尾加上 --show-output
告诉 Rust 显示成功测试的输出。
$ cargo test -- --show-output
有时运行整个测试集会耗费很长时间。如果你负责特定位置的代码,你可能会希望只运行与这些代码相关的测试。你可以向 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 one_hundredCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.69sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 1 test
test tests::one_hundred ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
我们可以指定部分测试的名称,任何名称匹配这个名称的测试会被运行。例如,因为头两个测试的名称包含 add
,可以通过 cargo test add
来运行这两个测试:
$ cargo test addCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.61sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... oktest result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
虽然可以通过参数列举出所有希望运行的测试来做到,也可以使用 ignore
属性来标记耗时的测试并排除他们
#[test]
#[ignore]
fn expensive_test() {// 需要运行一个小时的代码
}
我们在 #[test]
之后增加了 #[ignore]
行。现在如果运行测试,就会发现 it_works
运行了,而 expensive_test
没有运行:
$ cargo testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.60sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 2 tests
test expensive_test ... ignored
test it_works ... oktest result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
如果我们只希望运行被忽略的测试,可以使用 cargo test -- --ignored
,如果你希望不管是否忽略都要运行全部测试,可以运行 cargo test -- --include-ignored
$ cargo test -- --ignoredCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.61sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 1 test
test expensive_test ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
测试组织架构
Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试(unit tests)与 集成测试(integration tests)。单元测试倾向于更小而更集中,在隔离的环境中一次测试一个模块,或者是测试私有接口。而集成测试对于你的库来说则完全是外部的。它们与其他外部代码一样,通过相同的方式使用你的代码,只测试公有接口而且每个测试都有可能会测试多个模块。
- 单元测试
单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的某个单元的代码功能是否符合预期。
单元测试与他们要测试的代码共同存放在位于 src 目录下相同的文件中。规范是在每个文件中创建包含测试函数的 tests
模块,并使用 cfg(test)
标注模块。测试模块的 #[cfg(test)]
注解告诉 Rust 只在执行 cargo test
时才编译和运行测试代码,而在运行 cargo build
时不这么做,cfg 属性代表 configuration ,它告诉 Rust 其之后的项只应该被包含进特定配置选项中。在这个例子中,配置选项是 test
#[cfg(test)]
mod tests {#[test]fn it_works() {assert_eq!(2 + 2, 4);}
}
- 集成测试
在 Rust 中,集成测试对于你需要测试的库来说完全是外部的。同其他使用库的代码一样使用库文件,也就是说它们只能调用一部分库中的公有 API ,集成测试的目的是测试库的多个部分能否一起正常工作。
为了编写集成测试,需要在项目根目录创建一个 tests 目录,与 src 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个目录中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。
例子如下:
创建一个 tests 目录,新建一个文件 tests/integration_test.rs,并输入示例 11-13 中的代码。文件名: tests/integration_test.rs,与单元测试不同,我们需要在文件顶部添加 use adder
。这是因为每一个 tests
目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。tests
文件夹在 Cargo 中是一个特殊的文件夹, Cargo 只会在运行 cargo test
时编译这个目录中的文件。
use adder;#[test]
fn it_adds_two() {assert_eq!(4, adder::add_two(2));
}
也可以使用 cargo test
的 --test
后跟文件的名称来运行某个特定集成测试文件中的所有测试:
$ cargo test --test integration_testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.64sRunning tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)running 1 test
test it_adds_two ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 目录创建集成测试并使用 extern crate
导入 src/main.rs 中定义的函数。只有库 crate 才会向其他 crate 暴露了可供调用和使用的函数;二进制 crate 只意在单独运行。