11.1 编写和运行测试
11.1.1. 什么是测试
在Rust里,测试就是一个函数,用于验证非测试代码的行为是否符合预期。
一个测试函数通常执行三个操作:
- 准备(Arrange)数据/状态
- 运行(Act)被测试的代码
- 断言(Assert)结果
这三个操作在有些语言里叫3A步骤。
11.1.2. 解剖测试函数
测试函数本质上仍然是一个函数;不同之处在于它必须用test属性(attribute)进行标注。
Attribute就是一段Rust代码的元数据。它不会改变被它修饰的代码的逻辑,只是对代码进行修饰,或者说标注。实际上在 5.2. struct使用例(加打印调试信息) 中就已经用到过。
在函数上加#[test],就可以把函数变为测试函数。
11.1.3. 运行测试
先不管测试函数内的内容,编写完测试函数之后,如何运行它呢?使用cargo test命令来运行所有测试。
这个命令会构建一个测试运行器(test runner)可执行文件。它会逐个运行标注了test的函数,并报告它们是否成功。
当使用Cargo创建library项目时,会生成一个test module,里面有一个现成的test函数,可以参照它来编写其他测试函数。实际上,你可以添加任意数量的test module或test函数。
看个例子:
创建一个名为adder的新库项目:
$ cargo new adder --lib
Creating library `adder` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
$ cd adder
打开项目(lib.rs):
#![allow(unused)]
fn main() {
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]标注了,而不是因为它位于test模块中。test模块里也可以包含普通函数。
使用cargo test来运行测试:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.12s
Running unittests src/lib.rs (target/debug/deps/adder-302521ba8d0f0bdf)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
分析一下这个输出:
- 首先是编译(Compiling)、完成(Finished)和运行(Running)。
- 接下来是
running 1 test,表示正在执行一个测试。下一行显示这个测试是tests::it_works,结果是ok。这个项目只有一个测试,但如果有多个测试,cargo test会全部运行。 - 然后是
test result: ok.,表示项目中的所有测试都通过了。具体来说,1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out表示1个通过、0个失败、0个被忽略、0个基准测试、0个被过滤掉。 Doc-tests adder指的是文档测试的结果。Rust能够编译出现在API文档中的代码,这有助于保证文档始终与实际代码保持同步。
如果把函数改名,输出哪里会变呢?
#![allow(unused)]
fn main() {
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() { // 改名为 exploration
let result = add(2, 2);
assert_eq!(result, 4);
}
}
}
输出:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.08s
Running unittests src/lib.rs (target/debug/deps/adder-302521ba8d0f0bdf)
running 1 test
test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
可以看到,测试名从tests::it_works变成了tests::exploration。
11.1.4. 测试失败
测试函数一旦触发panic!,测试就失败。由于每个测试都在自己的线程中运行,主线程会监视这些线程。当主线程看到某个测试因触发panic!而崩溃时,那个测试就会被标记为失败。
看个例子:
#![allow(unused)]
fn main() {
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
fn another() {
panic!("Make this test fail");
}
}
}
这个another函数直接调用了panic!。运行一下看结果:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.08s
Running unittests src/lib.rs (target/debug/deps/adder-302521ba8d0f0bdf)
running 2 tests
test tests::exploration ... ok
test tests::another ... FAILED
failures:
---- tests::another stdout ----
thread 'tests::another' (448960) panicked at src/lib.rs:17:9:
Make this test fail
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::another
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
tests::another失败了,而tests::exploration仍然是ok。失败原因是thread 'tests::another' panicked at src/lib.rs:17:9,意思是在src/lib.rs的第17行第9列触发了panic!,也就是源代码中写该宏的位置。
总结一下,test result: FAILED表示整体测试运行失败。更具体地说,是1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out。