Rust编程语言入门之编写自动化测试
编写自动化测试
一、编写和运行测试
测试(函数)
- 测试:
- 函数
- 验证非测试代码的功能是否和预期一致
- 测试函数体(通常)执行的3个操作:
- 准备数据/状态
- 运行被测试的代码
- 断言(Assert)结果
解剖测试函数
- 测试函数需要使用 test 属性(attribute)进行标注
- Attribute就是一段Rust代码的元数据
- 在函数上加 #[test],可把函数变成测试函数
运行测试
-
使用 cargo test 命令运行所有测试函数
- Rust会构建一个 Test Runner 可执行文件
- 它会运行标注了 test 的函数,并报告其运行是否成功
-
当使用 cargo 创建 library 项目的时候,会生成一个 test module,里面有一个test 函数
- 你可以添加任意数量的 test module 或 函数
~/rust
➜ cargo new adder --lib
Created library `adder` package
~/rust
➜ cd adder
adder on master [?] via 🦀 1.67.1
➜ code .
adder on master [?] via 🦀 1.67.1 took 2.2s
➜
~/rust
➜ cargo new adder --lib
Created library `adder` package
~/rust
➜ cd adder
adder on master [?] via 🦀 1.67.1
➜ code .
adder on master [?] via 🦀 1.67.1 took 2.2s
➜
lib.rs 文件
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
测试失败
- 测试函数 panic 就表示失败
- 每个测试运行在一个新线程
- 当主线程看见某个测试线程挂掉了,那个测试标记为失败了。
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
fn another() {
panic!("Make this test fail")
}
}
运行
adder on master [?] is 📦 0.1.0 via 🦀 1.67.1 took 3.0s
➜ cargo test
Compiling adder v0.1.0 (/Users/qiaopengjun/rust/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.16s
Running unittests src/lib.rs (target/debug/deps/adder-6058f7b13179a51e)
running 2 tests
test tests::exploration ... ok
test tests::another ... FAILED
failures:
---- tests::another stdout ----
thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:17:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::another
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
adder on master [?] is 📦 0.1.0 via 🦀 1.67.1
二、断言(Assert)
使用 Assert! 宏检查测试结果
- assert! 宏,来自标准库,用来确定某个状态是否为true
- true:测试通过
- false:调用 panic!,测试失败
#[derive(Debug)]
pub struct Rectangle {
length: u32,
width: u32,
}
impl Rectangle {
pub fn can_hold(&self, other: &Rectangle) -> bool {
self.length > other.length && self.width > other.width
}
}
#[cfg(test)]
mod tests {
use super::*
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
length: 8,
width: 7,
};
let smaller = Rectangle {
length: 5,
width: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn samller_cannot_hold_larger() {
let larger = Rectangle {
length: 8,
width: 7,
};
let smaller = Rectangle {
length: 5,
width: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
使用 assert_eq! 和 assert_ne! 测试相等性
- 都来自标准库
- 判断两个参数是否相等或不等
- 实际上,它们使用的就是 == 和 !== 运算符
- 断言失败,自动打印出两个参数的值
- 使用 debug 格式打印参数
- 要求参数实现了 PartialEq 和 Debug Traits (所有的基本类型和标准库里大部分类型都实现了)
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
// assert_eq!(4, add_two(2));
assert_ne!(5, add_two(2));
}
}
三、自定义错误消息
添加自定义错误信息
- 可以向 assert!、assert_eq!、assert_ne! 添加可选的自定义消息
- 这些自定义消息和失败消息都会打印出来
- assert!:第 1 参数必填,自定义消息作为第2个参数。
- assert_eq! 和 assert_ne!:前2个参数必填,自定义消息作为第 3 个参数。
- 自定义消息参数会被传递给 format! 宏,可以使用 {} 占位符
pub fn greeting(name: &str) -> String {
//format!("Hello {}!", name)
format!("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greetings_contain_name() {
let result = greeting("Carol");
// assert!(result.contains("Carol"));
assert!(
result.contains("Carol"),
"Greeting didn't contain name, value was '{}'", result
);
}
}
四、用 should_panic 检查恐慌
验证错误处理的情况
- 测试除了验证代码的返回值是否正确,还需验证代码是否如预期的处理了发生错误的情况
- 可验证代码在特定情况下是否发生了 panic
- should_panic 属性(attribute):
- 函数 panic:测试通过
- 函数 没有 panic:测试失败
pub struct Guess {
value: u32,
}
impl Guess {
pub fn new(value: u32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value)
}
Guess {value}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
让 should_panic 更精确
- 为 should_panic 属性添加一个可选的 expected 参数:
- 将检查失败消息中是否包含所指定的文字
pub struct Guess {
value: u32,
}
impl Guess {
pub fn new(value: u32) -> Guess {
if value < 1 {
panic!("Guess value must be greater than or equal to 1, got {}.", value)
} else if value > 100 {
panic!("Guess value must be less than or equal to 100, got {}.", value)
}
Guess {value}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Guess value must be less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
五、在测试中使用 Result<T, E>
在测试中使用 Result<T, E>
- 无需 panic,可使用 Result<T, E> 作为返回类型编写测试:
- 返回 Ok:测试通过
- 返回 Err:测试失败
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
- 注意:不要在使用 Result<T, E> 编写的测试上标注 #[should_panic]
六、控制测试运行:并行和连续执行测试
控制测试如何运行
- 改变 cargo test 的行为:添加命令行参数
- 默认行为:
- 并行运行
- 所有测试
- 捕获(不显示)所有输出,使读取与测试结果相关的输出更容易。
- 命令行参数:
- 针对 cargo test 的参数:紧跟 cargo test 后
- 针对测试可执行程序:放在 -- 之后
- cargo test --help
- cargo -- --help
并行运行测试
- 运行多个测试:默认使用多个线程并行运行
- 运行快
- 确保测试之间:
- 不会互相依赖
- 不依赖于某个共享状态(环境、工作目录、环境变量等待)
--test-threads 参数
- 传递给 二进制文件
- 不想以并行方式运行测试,或想对线程数进行细粒度控制
- 可以使用 --test-threads 参数,后边跟着线程的数量
- 例如:cargo test -- --test-threads=1
显示函数输出
- 默认,如测试通过,Rust的test库会捕获所有打印到标准输出的内容
- 例如,如果被测试代码中用到了 println!:
- 如果测试通过:不会再终端看到 println! 打印的内容
- 如果测试失败:会看到 println! 打印的内容和失败信息
fn prints_and_returns_10(a: i32) -> i32 {
println!("I got the value {}", a);
10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq!(10, value);
}
#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(5, value);
}
}
- 如果想在成功的测试中看到打印的内容: --show-output
七、控制测试运行:按名称运行测试
- 选择运行的测试:将测试的名称(一个或多个)作为 cargo test 的参数
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
assert_eq!(4, add_two(2));
}
#[test]
fn add_three_and_two() {
assert_eq!(5, add_two(3));
}
#[test]
fn one_hundred() {
assert_eq!(102, add_two(100));
}
}
- 运行单个测试:指定测试名
cargo test one_hundred
- 运行多个测试:指定测试名的一部分(模块名也可以)
cargo test add
八、控制测试运行:忽略测试
忽略某些测试,运行剩余测试
- ignore 属性(attribute)
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(4, 2 + 2);
}
#[test]
#[ignore]
fn expensive_test() {
assert_eq!(5, 1 + 1 + 1 + 1 + 1);
}
}
- 运行被忽略(ignore)的测试:
cargo test -- --ignored
九、测试组织:单元测试
测试的分类
- Rust对测试的分类:
- 单元测试
- 集成测试
- 单元测试:
- 小、专注
- 一次对一个模块进行隔离的测试
- 可测试 private 接口
- 集成测试:
- 在库外部。和其他外部代码一样使用你的代码
- 只能使用 public 接口
- 可能在每个测试中使用到多个模块
单元测试
#[cfg(test)] 标注
- tests 模块上的 #[cfg(test)] 标注:
- 只有运行 cargo test 才编译和运行代码
- 运行 cargo build 则不会
- 集成测试在不同的目录,它不需要 #[cfg(test)] 标注
- cfg:configuration (配置)
- 告诉Rust下面的条目只有在指定的配置选项下才被包含
- 配置选项test:由Rust提供,用来编译和运行测试
- 只有 cargo test 才会编译代码,包括模块中的 helper 函数 和 #[test] 标注的函数
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(4, 2 + 2);
}
}
测试私有函数
- Rust允许测试私有函数
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}
fn internal_adder(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(4, internal_adder(2, 2));
}
}
十、集成测试
- 在Rust里,集成测试完全位于被测试库的外部
- 目的:是测试被测试库的多个部分是否能正确的一起工作
- 集成测试的覆盖率很重要
Tests 目录
- 创建集成测试:tests 目录
- tests 目录下的每个测试文件都是单独的一个 crate
- 需要将被测试库导入
- 无需标注 #[cfg(test)],tests 目录被特殊对待
- 只有 cargo test , 才会编译 tests 目录下的文件
src/lib.rs 文件
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
// #[test]
// fn another() {
// panic!("Make this test fail")
// }
}
tests/integration_test.rs 文件
use adder;
#[test]
fn it_add() {
assert_eq!(5, adder::add(2, 3));
}
运行
adder on master [?] is 📦 0.1.0 via 🦀 1.67.1
➜ cargo test
Compiling adder v0.1.0 (/Users/qiaopengjun/rust/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.11s
Running unittests src/lib.rs (target/debug/deps/adder-6058f7b13179a51e)
running 1 test
test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-461b916f2718e782)
running 1 test
test it_add ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
adder on master [?] is 📦 0.1.0 via 🦀 1.67.1
运行指定的集成测试
- 运行一个特定的集成测试:cargo test 函数名
- 运行某个测试文件内的所有测试: cargo test --test 文件名
tests/another_integration_tests.rs 文件
use adder;
#[test]
fn it_adds2() {
assert_eq!(7, adder::add(3,4));
}
运行
adder on master [?] is 📦 0.1.0 via 🦀 1.67.1
➜ cargo test --test integration_test
Finished test [unoptimized + debuginfo] target(s) in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-461b916f2718e782)
running 1 test
test it_add ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
adder on master [?] is 📦 0.1.0 via 🦀 1.67.1
➜ cargo test --test another_integration_tests
Compiling adder v0.1.0 (/Users/qiaopengjun/rust/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.11s
Running tests/another_integration_tests.rs (target/debug/deps/another_integration_tests-0a89cbf68d5b375f)
running 1 test
test it_adds2 ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
adder on master [?] is 📦 0.1.0 via 🦀 1.67.1
集成测试中的子模块
- tests 目录下每个文件被编译成单独的 crate
- 这些文件不共享行为(与 src 下的文件规则不同)
adder on master [?] is 📦 0.1.0 via 🦀 1.67.1
➜ tree
.
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
├── target
│ ├── CACHEDIR.TAG
│ ├── debug
│ └── tmp
└── tests
├── another_integration_tests.rs
├── common
│ └── mod.rs
└── integration_test.rs
27 directories, 205 files
tests/common/mod.rs 文件
pub fn setup() {}
tests/another_integration_tests.rs 文件
use adder;
mod common;
#[test]
fn it_adds2() {
common::setup();
assert_eq!(7, adder::add(3,4));
}
tests/integration_test.rs 文件
use adder;
mod common;
#[test]
fn it_add() {
common::setup();
assert_eq!(5, adder::add(2, 3));
}
针对 binary crate 的集成测试
- 如果项目是 binary Crate,只含有 src/main.rs 没有 src/lib.rs:
- 不能在 tests 目录下创建集成测试
- 无法把 main.rs 的函数导入作用域
- 只有library crate 才能暴露函数给其它 crate 用
- binary crate 意味着独立运行
本文来自博客园,作者:寻月隐君,转载请注明原文链接:https://www.cnblogs.com/QiaoPengjun/p/17281543.html