做一个真实项目的时候,测试方面主要考虑:
- 单元测试。测试单个文件的正确性,包括私有接口和公开接口
- 集成测试。测试程序核心流程的正确性,测试公开接口
- 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 来编译。
总体而言,集成测试的关键点在于:
- 在crate根目录创建tests目录,把测试文件放在该目录下
- 每个测试函数标注#[test]属性即可
- 共享函数需要用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)); }
注意几点:
- ![feature(test)]这一行不是放在测试文件中,而是需要在main.rs或者lib.rs中加入#![feature(test)],说明本crate使用了实验性的feature:test
- 这里虽然使用了 extern crate test;,但是项目的 Cargo.toml 文件中依赖区并不需要添加对 test 的依赖
- 测试函数使用#[bench]标注即可,可以放在任何文件任何位置
- 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
发表回复