Rust 测试

2022-04-16 language rust

Rust 支持常规的单元测试、集成测试等,甚至还支持文档测试。

单元测试

单元测试需要放到 tests 而且带有 #[cfg(test)] 属性的模块中,测试函数要加上 #[test] 属性。

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
fn main() {
    println!("Hello, world!");
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
    }
}

当函数中出现 panic 时测试就失败了,如下是常用的断言宏:

  • assert!(expression) 当表达式的值是 falsepanic,也可以添加失败时的自定义信息。
  • assert_eq!(left, right)assert_ne!(left, right) 检验左右两边是否相等、不等。

如果某个函数预期出现 panic 可以通过如下方式配置。

#[test]
#[should_panic(expected = "Your panic info")]
fn test_add() {
    assert_eq!(add(1, 2), 3);
}

如果是类似 Result 这种可恢复的错误,那么可以通过如下方式测试,包括 Option 这种。

assert!(some_error().is_err());
assert!(some_error().is_err_and(|e| e.kind() == ErrKind::NotFound));

let e = some_error().unwrap_err();
assert_eq!(e.kind(), ErrKind::NotFound);

assert!(some_result().is_none());
assert!(some_result().is_some());
assert!(some_result().is_some_and(|x| x > 0));

然后通过 cargo test 命令执行所有测试用例,或者通过 cargo test test_add 指定某个测试用例,或者通过 cargo test add 匹配所有相关用例。

另外,还可以通过 #[ignore] 标记不执行某个用例,如果要执行则通过 cargo test -- --ignored 执行。

----- 控制并行度
cargo test -- --test-threads=1
----- 显示输出
cargo test -- --show-output
----- 运行某个指定的测试用例
cargo test foobar::hello::case
----- 查看所有用例
cargo test -- --list
----- 可以通过 --lib --doc 控制只运行库或者文档测试用例
cargo test --lib --package xxx

上述的 --package 参数指定的是目录下 Cargo.toml 中的 package.name 参数。

Fuzz

关于 Fuzz 测试可以参考 The Fuzzing Book 中的介绍,而 Rust 相关的更多内容可以参考 Rust Fuzz Book 中的介绍。

----- 方式安装升级
cargo install cargo-fuzz
cargo install --force cargo-fuzz

----- 查看帮助信息
cargo fuzz --help

----- 新建工程,一般会生成fuzz目录以及一个简单示例
cargo fuzz init
----- 也可以手动添加新的示例
cargo fuzz add <your-fuzz-name>

此时会生成对应的示例代码,可以根据项目的需要进行修改,包括用例的名称、依赖等。

----- 查看支持的测试列表
cargo fuzz list
----- 开始测试
cargo fuzz run <your-fuzz-name>

如果出现报错,会同时输出包括的数组内容,以及 Reproduce with 以及 Minimize test case with 两条命令,可以手动执行进行验证,可以有效缩短异常的场景。

libfuzzer

这也是默认的,通常会有如下的输出。

#19644289 REDUCE cov: 383 ft: 2112 corp: 639/89Kb lim: 4096 exec/s: 8031 rss: 650Mb L: 1080/3078 MS: 1 EraseBytes-

可以解读为:

  • #19644289 已经测试过的数据次数。
  • cov 当前已经覆盖的代码块。
  • ft 尝试覆盖信号个数。
  • corp 当前随机序列的入口个数,以及其所占用的内存大小。
  • lim 当前最大为 65535 字节,也就输入的长度。
  • exec/s 迭代速度,每秒多少次。
  • rss 当前内存消耗。
  • L 当前输入实际所占字节。

性能测试

上述的单元测试、Fuzz 测试以及集成测试主要是处理功能上的异常,包括不同版本迭代过程中,而性能测试则主要是防止性能上可能出现的回退。如下针对斐波那契数列进行简单的测试,这实际就是 1 1 2 3 5 8 ... 数列,当前值为前两个值相加。

这里使用 criterion 实现,通过 cargo add --dev criterion 添加依赖包,如果要生成 html 报告可以同时添加 features=["html_reports"] 特性,在 Cargo.toml 中添加测试,注意添加 harness 参数。

注意,html 报告依赖 gnuplot 命令,可以通过 gnuplot --version 确认是否安装,可以通过 apt-get install gnuplot 或者 yum install gnuplot 命令安装即可,会生成 target/criterion/report/index.html 报告文件。

[dev-dependencies]
criterion = {version="0.5.1", features=["html_reports"]}

[[bench]]
name = "foobar"
harness = false

如下是示例代码。

use std::hint::black_box;

use criterion::{criterion_group, criterion_main, Criterion};

fn fibonacci(n: i64) -> i64 {
    if n <= 0 {
        panic!("{} is invalid!", n);
    }
    match n {
        1 | 2 => 1,
        _ => fibonacci(n - 2) + fibonacci(n - 1),
    }
}

fn simple(c: &mut Criterion) {
    c.bench_function("fibonacci", |b| {
        b.iter(|| fibonacci(black_box(20)));
    });
}

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

另外,当通过 workspace 管理时,需要在对应目录下执行 cargo bench 命令,通过 -p 指定包名未生效,不确定原因。

性能比较

示例如下,会运行两个函数。

use std::hint::black_box;

use criterion::{criterion_group, criterion_main, Criterion};

fn fibonacci_v1(n: i64) -> i64 {
    if n <= 0 {
        panic!("{} is invalid!", n);
    }
    match n {
        1 | 2 => 1,
        _ => fibonacci_v1(n - 2) + fibonacci_v1(n - 1),
    }
}

fn fibonacci_v2(n: i64) -> i64 {
    if n <= 0 {
        panic!("{} is invalid!", n);
    } else if n == 1 {
        return 1;
    }

    let mut sum = 0;
    let mut last = 0;
    let mut curr = 1;
    for _i in 1..n {
        sum = last + curr;
        last = curr;
        curr = sum;
    }
    sum
}

fn simple(c: &mut Criterion) {
    let mut group = c.benchmark_group("fibonacci");
    group.bench_function("fibonacci v1", |b| {
        b.iter(|| fibonacci_v1(black_box(20)));
    });
    group.bench_function("fibonacci v2", |b| {
        b.iter(|| fibonacci_v2(black_box(20)));
    });
    group.finish();
}

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

还可以通过 --save-baseline 保存一个基准结果,此水会保存在 baseline.json 文件中,后续通过 --baseline 与此基准进行比较。

flamegraph

在 Linux 系统中,通常会使用 Perf 获取性能信息,不过 Rust 中可以使用 cargo install flamegraph 安装 Rust 相关的工具,然后通过如下命令运行即可。

cargo flamegraph --bench simple -o simple-baseline.svg -- --bench
cargo flamegraph --bin simple -o simple-baseline.svg

类似 HTTP 的压测可以使用 wrk 工具。

其它

nextest

建议使用 cargo-nextest 查看测试结果,要比原生更加方便使用,执行 cargo nextest run 即可,如下是常用的命令。

----- 运行所有测试用例
$ cargo nextest run
----- 运行某个指定用例
$ cargo nextest foobar::hello::case

文档测试

通常在文档中会维护一些示例代码,为了保证其正确性,可以通过 rustdoc 命令进行测试。

覆盖率

建议使用 tarpaulin 工具,相比 cargo-llvm-cov 来说功能要更丰富,可以通过 cargo install cargo-tarpaulin 安装,然后直接执行 cargo tarpaulin 命令即可。

可以通过 -o html 指定输出 Web 页面。