编写自动化测试
测试函数剖析
#[cfg(test)]: 作用于整个模块或代码块,控制是否进行编译.#[test]: 作用于具体的函数,告诉测试执行器这是一个需要被运行的测试用例#[ignore]:#[test]之后增加了#[ignore]行,执行cargo test将跳过该测试.可以通过cargo test -- --ignored单独运行被忽略的测试
#[cfg(test)]只有在运行cargo test时才编译这段代码.这可以减小最终发布包(cargo build --release)的体积,并加快正常编译速度
检查结果
assert!(bool, str): 为 true 时什么都不做,为 false 调用panic!宏,可以增加错误信息assert_eq!(a, b): 比较两个值是否相等assert_ne!(a, b): 比较两个値是否不相等
eq表示"equal"(相等),ne表示"not equal"(不相等)
使用 should_panic 检查 panic
#[should_panic]属性位于#[test]之后#[should_panic(expected = "错误信息")]: 可以精确匹配错误信息中包含的文本- 确保当程序接收到非法输入或进入非法状态时,能够正确地触发
panic! - 添加此属性后,只有当函数发生 panic 时,测试才会被视为通过(Passed)
在测试中使用 Result<T, E>
- 不能对这些使用
Result<T, E>的测试使用#[should_panic]注解 - 为了断言一个操作返回
Err成员,不要对Result<T, E>值使用问号表达式(?).而是使用assert!(value.is_err()) - 使用
Result的优势是可以在测试中使用?操作符,使代码更简洁
综合例子
rust
// 被测试函数
fn add(x: i32, y: i32) -> i32 {
x + y
}
#[test]
fn it_works() {
let result = add(1, 1);
assert_eq!(result, 2); // 断言结果等于2
}
#[test]
fn it_fails() {
let result = add(1, 1);
assert!(result == 2, "结果不相等: {}", result); // 为false时打印错误信息
}
// 会panic的函数
fn will_panic() {
panic!("崩溃了")
}
#[test]
#[should_panic(expected = "崩溃了")]
fn test_panic() {
will_panic();
}
// 返回Result的测试函数
#[test]
fn result() -> Result<(), String> {
let x = add(1, 1);
if x == 2 {
Ok(())
} else {
Err(format!("结果不等于2,实际值: {}", x))
}
}代码说明:
- 第一个测试使用
assert_eq!直接比较值 - 第二个测试使用
assert!配合错误信息 #[should_panic]中expected参数应与实际 panic 信息匹配- 使用
Result<T, E>的测试函数无需#[should_panic]属性
运行测试
测试命令
| 命令 | 针对对象 | 典型功能 |
|---|---|---|
cargo test --help | Cargo 编译管理器 | 选择测试目标(lib/bin/doc)、条件编译、依赖处理 |
cargo test -- --help | 测试运行程序 | 并行度控制、输出捕获、运行被忽略的测试 |
并行或顺序运行测试
- 当运行多个测试时,Rust 默认使用线程来并行运行
- 如果不希望测试并行运行,或想要精确控制线程数量,可以传递
--test-threads参数
bash
$ cargo test -- --test-threads=1 # 只使用一个线程,顺序运行测试显示函数输出
- 默认情况下,通过的测试中的打印值会被捕获.使用
--show-output显示成功测试的输出
bash
$ cargo test -- --show-output通过名称运行部分测试
运行单个测试
bash
$ cargo test 函数名过滤运行多个测试
bash
$ cargo test 函数名的一部分- 任何名称匹配这个部分的测试都会被运行
运行被忽略的测试
bash
$ cargo test -- --ignored- 只运行标记为
#[ignore]的测试
bash
$ cargo test -- --include-ignored- 同时运行所有测试(包括被忽略的)
测试的组织架构
单元测试(unit tests)
单元测试的目标是隔离地验证每一段代码(通常是一个函数或模块)的逻辑
- 存放位置: 直接写在源文件(.rs)中.通常放在文件的末尾,并包含在一个带有
#[cfg(test)]属性的模块内 - 私有访问: 单元测试可以访问所属模块中的私有函数(由于
use super::*;) - 编译行为: 只在运行
cargo test时编译,不影响发布包体积
单元测试示例:
rust
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 2), 4);
}
}集成测试(integration tests)
集成测试的目标是验证多个库部分是否协同工作.它像"外部用户"一样调用你的代码
- 存放位置: 存放在项目根目录下的
tests/目录中(与src平级).每个.rs文件都会被编译为一个独立的 crate - 私有访问: 不能访问私有函数,只能测试库导出的
pub(公共)API - 适用范围:仅适用于库项目(Library).纯二进制项目(只有
main.rs)无法直接使用tests/目录进行集成测试 - 编译行为: 编译为独立的可执行二进制文件
集成测试示例:
rust
// tests/integration_test.rs
use my_crate;
#[test]
fn test_public_api() {
assert_eq!(my_crate::add(3, 2), 5);
}对比
| 特性 | 单元测试 | 集成测试 |
|---|---|---|
| 存放位置 | src/ 内部文件末尾 | tests/*.rs 独立文件 |
| 属性标记 | #[cfg(test)] | 无需标记(文件夹特性) |
| 访问权限 | 可访问 pub 和私有成员 | 仅能访问 pub 成员 |
| 测试范围 | 细粒度、单个函数/模块 | 粗粒度、功能流、对外接口 |
| 编译行为 | 与源码一起编译 | 编译为独立的二进制文件 |
| 适用项目 | 库和二进制项目 | 仅库项目 |
文档测试
- 文档测试 (Doc-tests) 是直接写在代码注释中的代码示例,它们会被
cargo test像真正的测试用例一样执行 - 其核心思想是: "文档即测试",确保你给用户的示例代码永远不会因为 API 的变动而失效
基本用法
- 书写位置: 位于
///类型的文档注释中,通常在# Examples标题下,并包裹在 Markdown 的代码块(三个反引号)里 - 验证一致性: 如果代码改了名,而文档里的例子没改,
cargo test会直接报错,防止误导用户 - 库项目专用: 主要用于 Library 项目,Binary 项目(main.rs)通常不支持文档测试
示例
rust
/// 将两个数字相加.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}详细说明
1. 文档注释标记 ///
- 与普通的
//不同,///是文档注释 - Rust 编译器(rustdoc)会提取这些内容,生成 HTML 格式的项目 API 文档
- 必须写在要说明的项(如函数)之前
2. Markdown 格式
- 注释内部完全支持 Markdown 语法
# Examples是二级标题,在生成的 HTML 文档中会显示为独立栏目
3. 代码块与隐式 Main
- 代码块中的代码会被当作独立程序运行
- Rust 在后台自动将代码包裹进虚拟的
fn main() { ... }中 my_crate::add_one: 需要像外部用户一样引用包名
文档测试的控制
- 用于包裹文档测试的代码块
| 语法 | 作用 |
|---|---|
```rust | 标准的可运行文档测试 |
```ignore | 显示代码但不运行 |
```no_run | 编译但不运行(用于耗时操作) |
```should_panic | 测试应该 panic 的代码 |
```compile_fail | 测试代码应该编译失败 |
相关命令
bash
$ cargo test --doc # 只运行文档测试
$ cargo doc --open # 生成并打开 API 文档
$ cargo doc --open --document-private-items # 包含私有项的文档在
lib.rs中的函数默认是pub的,无需--document-private-items参数
main.rs中的函数通常是私有的,需要该参数查看文档