编写自动化测试
Edsger W. Dijkstra 在 1972 年的文章"The Humble Programmer"中说:"程序测试可以非常有效地证明 bug 的存在,但对于证明 bug 不存在却无能为力。"这不意味着我们不应该尽可能多地测试!
程序中的_正确性_是指代码按照我们的意图执行的程度。Auto 非常关注正确性 —— 类型系统、所有权规则和借用检查器在编译时就捕获了许多 bug。但类型系统无法捕获所有问题。这就是自动化测试发挥作用的地方。
Auto 的测试系统与 Rust 紧密对齐,因为 Auto 转译为 Rust 并使用相同的测试运行器来运行测试。本章涵盖 Auto 中可用的测试机制。
如何编写测试
_测试_是验证非测试代码按预期运行的函数。测试函数通常执行以下三个操作:
- 设置所需的数据或状态。
- 运行你要测试的代码。
- 断言结果符合预期。
测试函数的结构
Auto 中的测试是用 #[test] 标注的函数。当你运行 automan test 时,转译器将代码编译为 Rust 并运行 cargo test,它会构建一个测试运行器二进制文件来执行被标注的函数。
#[test] 属性将函数标记为测试。assert_eq! 宏检查两个值是否相等。如果不相等,测试会 panic 并失败。
用 assert! 检查结果
assert! 宏验证条件是否为 true。如果条件为 false,测试失败:
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle(8, 7)
let smaller = Rectangle(5, 1)
assert(larger.can_hold(&smaller))
}添加自定义失败消息
你可以在 assert! 和 assert_eq! 中添加自定义消息,使失败更容易调试:
当这个测试失败时,会打印自定义消息,显示 result 的实际值 —— 使调试变得更容易。
用 #[should_panic] 检查 Panic
要测试代码在某些条件下会 panic,使用 #[should_panic] 属性:
#[test]
#[should_panic]
fn it_panics() {
panic("expected panic")
}你还可以用 expected 检查特定的 panic 消息:
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess.new(200)
}在测试中使用 !T(Result)
测试可以返回 !T(Result)而不是 panic。这让你可以在测试函数中使用 .!(错误传播):
注意:fn valid_guess() ! 转译为 fn valid_guess() -> Result<(), String>。 这让你可以在测试中使用 ?(通过 .!)进行错误传播。
测试语法速查
Auto Rust 用途 #[test]#[test]将函数标记为测试 assert_eq(a, b)assert_eq!(a, b)断言相等 assert_ne(a, b)assert_ne!(a, b)断言不等 assert(cond)assert!(cond)断言条件为真 #[should_panic]#[should_panic]测试应该 panic #[should_panic(expected = "...")]#[should_panic(expected = "...")带特定消息的 panic fn test() !fn test() -> Result<(), String>返回错误而非 panic
控制测试的运行方式
使用 automan test(它会调用转译后的 Rust 代码上的 cargo test)运行测试:
$ automan test并行或顺序运行测试
测试默认并行运行。要逐个运行:
$ automan test -- --test-threads=1显示输出
要查看通过测试的 print() 输出:
$ automan test -- --show-output运行测试的子集
运行名称包含特定模式的测试:
$ automan test add # 运行所有名称包含 "add" 的测试运行单个测试:
$ automan test it_adds_two忽略耗时测试
使用 #[ignore] 跳过耗时的测试:
#[test]
#[ignore]
fn expensive_test() {
// 耗时较长的代码
}只运行被忽略的测试:
$ automan test -- --ignored测试组织
Auto 遵循与 Rust 相同的测试组织方式:
单元测试
单元测试放在与被测代码相同的文件中,在 #[cfg(test)] 标注的 tests 模块内:
此示例依赖多文件模块,不能在书页内运行——请到 Playground
fn internal_adder(a int, b int) int {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
assert_eq(internal_adder(2, 2), 4)
}
}单元测试可以测试私有函数,因为 tests 模块是子模块,可以通过 use super::* 访问父模块的私有项。
集成测试
集成测试放在项目根目录的 tests/ 目录中。tests/ 中的每个文件被编译为单独的 crate,只测试公共 API:
my_project/
├── auto.toml
├── src/
│ └── lib.rs
└── tests/
└── integration_test.rs集成测试使用 use 从你的库中导入:
此示例依赖多文件模块,不能在书页内运行——请到 Playground
use my_project::add_two
#[test]
fn it_adds_two() {
assert_eq(add_two(2), 4)
}总结
Auto 的测试系统通过转译利用了 Rust 成熟的测试基础设施。要点如下:
| 概念 | Auto | 命令 |
|---|---|---|
| 运行所有测试 | — | automan test |
| 单个测试 | #[test] | automan test test_name |
| 过滤测试 | — | automan test pattern |
| 单线程运行 | — | automan test -- --test-threads=1 |
| 显示输出 | — | automan test -- --show-output |
| 运行被忽略的测试 | #[ignore] | automan test -- --ignored |
测试宏(assert_eq!、assert!、assert_ne!)在 Auto 和 Rust 中工作方式完全相同。Auto 的 fn test() ! 提供了一种简洁的方式,使用与整个代码库相同的 !T 语法来编写传播错误的测试。
现在让我们结合前面章节学到的知识来构建一个项目!