rust中自动化测试

做一个真实项目的时候,测试方面主要考虑:

  • 单元测试。测试单个文件的正确性,包括私有接口和公开接口
  • 集成测试。测试程序核心流程的正确性,测试公开接口
  • benchmark测试。测试程序的性能

1. 单元测试

单元测试主要测试单个文件的正确性,因此总是和文件放在一起。golang的风格我非常赞同,在每一个a.go之外,可以定一个a_test.go作为单元测试,非常直白。

在rust中,测试代码一般是和程序写在一起的。比如有个calc.rs程序:

fn mul(x:i32,y:i32)->i32{
    x * y
}

其中单元测试代码也一起写在calc.rs中:

fn mul(x:i32,y:i32)->i32{
    x * y
}

#[cfg(test)]
mod tests{
    use super::*;

    #[test]
    fn test_mul(){
        assert_eq!(mul(1,2), 2)
    }
}

注意#[cfg(test)],表明该mod通过“cargo test” 执行:

cargo test

函数还使用了#[test]属性,表示该函数是一个独立的测试函数。也可以不带#[test]属性的函数,此时该函数一般作为辅助功能,并不是独立的测试函数。比如

fn mul(x:i32,y:i32)->i32{
    x * y
}

#[cfg(test)]
mod tests{
    use super::*;

    fn setup(){
        assert_ne!(1+2, 2);
    }

    #[test]
    fn test_mul(){
        setup();
        assert_eq!(mul(1,2), 2)
    }
}

setup就不是一个独立的测试函数,它会被其他测试函数调用。

还有一个问题,为啥不把测试代码放在单独的文件calc_test.rs呢?我认为是为了测试私有接口,因为私有接口无法被本文件之外访问到。

多个mod

注意上面的测试文件中有一个mod tests,tests只是任意取的mod名,但其实可以有多个mod:

pub fn add(x:i32, y:i32)->i32{
    x+y
}

pub fn sub(x:i32, y:i32)->i32{
    x-y
}

fn mul(x:i32,y:i32)->i32{
    x * y
}

#[cfg(test)]
mod test_1{
    use super::*;
    #[test]
    fn test_sub(){
        assert_eq!(sub(2,1), 1)
    }

    #[test]
    fn test_add(){
        assert_eq!(add(1,2), 3)
    }
}


#[cfg(test)]
mod test_2{
    use super::*;
    #[test]
    fn test_mul(){
        assert_eq!(mul(1,2), 2)
    }
}

现在我们有两个mod, test_1, test2,可以分别指定要测试哪个mod:

cargo test test_1 
cargo test test_2

若只是运行cargo test,则会测试所有的mod。

不同的mod可以分布在多个文件中,比如calc.rs中有mod test_1:

pub fn add(x:i32, y:i32)->i32{
    x+y
}

#[cfg(test)]
mod test_1{
    use super::*;

    #[test]
    fn test_add(){
        assert_eq!(add(1,2), 3)
    }
}

calc2.rs中也有mod test_1:

pub fn sub(x:i32, y:i32)->i32{
    x-y
}

#[cfg(test)]
mod test_1{
    use super::*;
    #[test]
    fn test_sub(){
        assert_eq!(sub(2,1), 1)
    }
}

运行cargo test test_1时,会执行两个文件中的test_1。原因是cargo test指令是一种模式匹配。可以指定上面的mod名称,也可以指定文件名:

cargo test cacl

还可以指定函数名:

cargo test test_a::test_mul

此外,我们还可以用ignore忽略测试,被ignore标注的函数在cargo test时不会执行,但可以通过cargo test — –ignored的方式强制执行:

#[test]
#[ignore]
fn expensive_test() {
    // 这里的代码需要几十秒甚至几分钟才能完成
}

2. 集成测试

一个标准的 Rust 项目,在它的根目录下会有一个 tests 目录。这里面就是放集成测试代码的,和单元测试有所不同。

tests 目录默认与 src 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个目录中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。

总体而言,集成测试的关键点在于:

  1. 在crate根目录创建tests目录,把测试文件放在该目录下
  2. 每个测试函数标注#[test]属性即可
  3. 共享函数需要用mod管理,创建mod目录和mod.rs。原因是集成测试时,每个测试文件都视为单独的crate

集成测试这篇文章写得很好:

https://rustwiki.org/zh-CN/book/ch11-03-test-organization.html#%E9%9B%86%E6%88%90%E6%B5%8B%E8%AF%95

需要注意的时候,集成测试只能对lib有效。

如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 目录测试 src/main.rs 中定义的函数。但可以在tests目录中测试所引用本地lib中的函数,这也是常见的做法。

3. benchmark基准测试

基准测试有两种方式, 一种是使用rust自带的基准测试功能,很遗憾,这种功能目前处于实验阶段,在stable版本里面无法使用,需要使用nightly版本。

#![feature(test)]
extern crate test;
use test::Bencher;

#[bench]
fn bench_add_two(b: &mut Bencher) {
    b.iter(|| add(1,2));
}

注意几点:

  1. ![feature(test)]这一行不是放在测试文件中,而是需要在main.rs或者lib.rs中加入#![feature(test)],说明本crate使用了实验性的feature:test
  2. 这里虽然使用了 extern crate test;,但是项目的 Cargo.toml 文件中依赖区并不需要添加对 test 的依赖
  3. 测试函数使用#[bench]标注即可,可以放在任何文件任何位置
  4. cargo +nightly bench运行基准测试

第三方基准库

有很多第三方基准库,弥补官方不正式支持bench的缺陷。这里介绍criterion:https://crates.io/crates/criterion

首先在cargo.toml中引入:

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

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

然后建立benches/my_benchmark.rs或者benches/my_benchmark/main.rs文件,注意,这是默认文件路径,可以通过cargo.toml中的bench字段修改。

文件示例如下:

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

fn fibonacci(n: u64) -> u64 {
    match n {
        0 => 1,
        1 => 1,
        n => fibonacci(n-1) + fibonacci(n-2),
    }
}

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);    

运行cargo bench即可

4. 控制test范围

通过执行cargo test时,会运行本crate单元测试和集成测试,而不包括子crate的。

如果项目中包含子crate,而打算单独测子crate时,运行时就需要-p参数,例如:

cargo test -p mylib

此外cargo test默认会运行所有的单元测试和集成测试,但也可以改变其行为。

bench测试同理。

1. cargo中配置test

还可以在cargo中配置test文件,如下:

[[test]]
name = "unit"
path = "tests/unit.rs"

[[test]]
name = "integration"
path = "tests/integration.rs"

[[bench]]
name = "sorting"
path = "benches/sorting.rs"

[[bench]]
name = "searching"
path = "benches/searching.rs"

在这种情况下,运行 cargo test 命令时,只会执行 tests/unit.rs 和 tests/integration.rs 这两个路径下的测试文件。其他位置的测试文件将被忽略。

bench测试同理。

2. 仅执行单元测试

可以仅仅执行单元测试,不执行集成测试:

cargo test --lib
cargo test --bin
cargo test --bins
cargo test --lib --bins
cargo test -p mylib --lib

3. 仅执行集成测试

也可通过–test参数,仅执行集成测试,不执行单元测试:

cargo test --test test1
cargo test -p mylib --test test1
cargo test --test '*'

4. 仅执行基准测试

cargo bench 
cargo bench -p mylib


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

About Me

一位程序员,会弹吉他,喜欢读诗。
有一颗感恩的心,一位美丽的妻子,两个可爱的女儿
mail: geraldlee0825@gmail.com
github: https://github.com/lisuxiaoqi
medium: https://medium.com/@geraldlee0825