在Rust中进行集成测试

集成测试是一种独立于代码的测试方式,进行集成测试时,相当于从外部调用库或运行程序,它常用于测试项目整体,而非独立的单元。

集成测试的代码放在根目录下的tests目录中,目录下的每个文件都是独立的包,这意味着测试项目时,就好像使用外部的包一样,需要独立导入,且每个测试文件之间也是独立的。

准备

一些库经常在集成测试中使用,在cargo.toml中加入这些开发依赖:

1
2
3
4
5
[dev-dependencies]
assert_cmd = "2.0.7"
predicates = "2.1.3"
tempfile = "3.3.0"
criterion = "0.4.0"

截至文章更新时间,这里列出的版本号以及下文的用法均对应最新版本。

使用tempfile创建临时目录和文件

我们在测试时常常需要使用一些文件与目录,使用临时文件会比使用根目录下指定的文件更好。tempfile包提供了生成临时文件的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use tempfile::{tempfile, tempdir};

// tempfile返回一个标准库中的File对象
let mut file1 = tempfile()?;
// tempdir返回一个TempDir对象,它不是Rust标准库的部分
let mut dir = tempdir()?;

// 常使用path()方法获取路径,进行其他操作
let mut file2 = File::create(dir.path().join("hello.txt"))?;

// Builder提供了更多生成文件的方法,例如生成随机文件名
let mut rand_file = Builder::new()
.suffix(".txt")
.rand_bytes(5)
.tempfile()?;

使用assert_cmd测试可执行程序

集成测试中常常需要直接测试可执行程序而非某个库功能,此时可以使用assert_cmd

假定我们构建的可执行程序的文件名为hello.exe,使用cargo运行需要的指令为cargo run --bin hello

使用assert_cmd中的Command::cargo_bin("hello")即可达到同样的效果。

控制测试环境

我们常常需要在测试时指定一些环境变量或标准输入内容。Command结构体提供了一些方法,可以定制运行环境。

1
2
3
4
5
6
7
8
9
10
11
12
use asser_cmd::Command;
use tempfile::tempdir;
use time::Duration;

let temp_dir = tempdir()?;
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))
.unwrap()
.current_dir(&temp_dir) // 指定运行路径
.env("RUST_LOG", "debug") // 指定环境变量,也可使用envs()方法
.arg("-V") // 指定参数,也可使用args()方法
.timeout(Duration::from_secs(1)) // 期望程序在1秒内执行完毕
.write_stdin("Hello, world!"); // 写入标准输入

使用AssertPredicate判断测试结果

我们需要获取程序运行的结果并测试是否符合预期,这时可以对Command调用assert()方法获取Assert对象,通过Assert来测试结果。

例如,我们编写了一个从标准输入中读取一个数字,并输出它的平方的程序,可以使用如下方法进行测试。

1
2
3
4
5
6
7
use assert_cmd::Command;

let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))
.unwrap()
.write_stdin("20")
.assert() // 获取assert对象
.stdout("400"); // 判断输出内容是否为"400"

stdout方法的签名为:

1
2
3
4
pub fn stdout<I, P>(self, pred: I) -> Self
where
I: IntoOutputPredicate<P>,
P: Predicate<[u8]>

Predicatepredicate_core中规范的trait,当我们传入"400"时,表示输出内容与其完全匹配。

predicates库包含许多功能强大的Predicate,例如部分匹配,假定我们想要测试使用-V参数后,输出的内容中是否包含程序版本号,我们可以编写如下测试:

1
2
3
4
5
6
7
8
use assert_cmd::Command;
use predicates::str;

let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))
.unwrap()
.arg("-V")
.assert()
.stdout(str::contains(env!("CARGO_PKG_VERSION"))); // 测试是否包含版本号

除此之外,还常用Assert判断程序是否正确退出。

1
2
3
4
5
6
7
Command::cargo_bin("bin_fixture")
.unwrap()
.env("exit", "42")
.assert()
.sucess(); // 成功运行
// .failure() // 运行出错
// .code(1) // 错误码为1

手动判断测试结果

当我们需要对输出进行复杂的判断时,AssertPredicate可能无法满足我们的需求。我们可以在Command上调用output方法获取Ouput对象,这个对象保存了程序的stdoutstderr输出,获取输出后可以手动编写判断语句。

1
2
3
4
let mut output = Command::cargo_bin(env!("CARGO_PKG_NAME"))
.unwrap()
.output();
assert!(String::from_utf8(output.stdout)?.contains("114514"));

基准测试

nightly的Rust自带cargo bench基准测试功能,但自带的基准测试事实上已经被deprecated了,不会出现在stable中。更推荐使用criterion库进行基准测试。

1
2
3
4
5
6
pub fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);