测试是保证软件质量的关键一环,这一节主要讲 Cargo 还有怎么写测试,也包括如何为代码写文档,如何评估代码的性能。
断言
基本上单元测试都会通过断言来判断是否输出相同的预期结果:
布尔值:
assert!(true); // 最简单的断言
assert!(a==b, "{} was not equal to {}", a, b);
let a = 23;
let b = 87;
assert_eq!(a, b, "{} and {} are not equal", a, b); // 相等
assert_ne!(); // 不等
debug_assert!
:类似于 assert!
,但这个不是放在专门测试代码里的,是放在业务代码里的。在默认的 Debug 开发下,可以给出一些断言来验证执行过程中的结果的正确性。
单元测试
单元测试是轻量级的,可以快速进行的测试,针对都是一个独立的小功能进行测试,例如函数。
最简单的 Rust 单元测试:
// first_unit_test.rs
#[test]
fn basic_test() {
assert!(true);
}
生成二进制可执行文件:rustc --test first_unit_test.rs
。
运行该二进制可执行文件,结果:
➜ rust_projects ./first_unit_test
running 1 test
test basic_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
所有的测试默认是并行的,除非运行的时候通过环境变量指定测试的时候只要一个线程:RUST_TEST_THREADS=1
。
RUST_TEST_THREADS=1 ./first_unit_test
隔离测试代码
测试越来越复杂的时候,我们希望能把测试代码和程序逻辑代码分开。那么可以把所有与测试相关的代码放到一个模块里。这时候可以使用使用 #[cfg(test)]
。
cfg
用于条件编译,也不是仅仅可以用在测试代码里。它可以通过 flag 决定包含或者排除该部分代码。在 #[cfg(test)]
中,flag 就是 test
。意思就是说,只有运行 cargo test
的时候,测试代码才会被包含和编译进去。
比如说,编写一个函数用于生成测试用例,但你肯定不希望这个函数被包含在实际代码里。
先用 cargo 创建一个项目 cargo new unit_test --lib
,之后 lib.rs 代码修改如下(可以看到创建项目生成的样例里的测试就有 #[cfg(test)]
):
fn sum(a: i8, b: i8) -> i8 {
a + b
}
#[cfg(test)]
mod tests {
fn sum_inputs_outputs() -> Vec<((i8, i8), i8)> {
vec![((1, 1), 2), ((0, 0), 0), ((2, -2), 0)]
}
#[test]
fn test_sum() {
for (input, output) in sum_inputs_outputs() {
assert_eq!(crate::sum(input.0, input.1), output);
}
}
}
sum_inputs_outputs
用来生成输入和输出的测试用例,被用在 test_sum()
中。但是,要注意到这个函数没有 #[test]
注解。#[test]
属性可以使得代码不在最终发布的代码中。然而,sum_inputs_outputs
没有标记 #[test]
,但它放在了 test
模块中。
未通过测试
有一些情况下,你可能希望针对特定的输入是不能通过测试(失败)的,那么可能就想要测试框架可以断言在这种情况下是失败的。Rust 提供了一个 #[should_panic]
就是干这活的。
#[test]
#[should_panic]
fn this_panics() {
assert_eq!(1, 2);
}
忽略测试
如果从持续集成,或者说敏捷开发的角度,是不应该忽略测试的,更不应该随便删除测试代码。如果你确定不想使用某个测试,或者说整个测试特别的重量级(或许应该考虑重构测试了),那么可以使用 #[ignore]
。
pub fn silly_loop() {
for _ in 1..1_0000_0000 {
}
}
#[cfg(test)]
mod tests {
#[test]
#[ignore]
fn test_silly_loop() {
crate::silly_loop();
}
}
集成测试
单元测试可以用来测试私有接口和独立模块,集成测试则更像端到端的黑盒测试,针对的是公有的接口。从代码角度,两种并没有太大的区别。
新建一个库项目:
carg