Skip to content
📖 Auto 编程语言(主书)本书目录

测试 ​

在 1972 年的文章《谦卑的程序员》中,Edsger W. Dijkstra 写道:"程序测试可以是 展示 bug 存在的非常有效的方法,但对于展示 bug 不存在来说,则是完全不够的。" 这并不意味着我们不应该尽可能地多写测试。

我们程序中的_正确性_是指代码做我们期望它做的事情的程度。Auto 在设计时高度 关注正确性——类型系统、所有权规则和借用检查器在编译时就能捕获许多 bug。但类型 系统无法捕获所有问题。这就是自动化测试发挥作用的地方。

Auto 的测试系统与 Rust 的测试系统紧密对齐,因为 Auto 会转译为 Rust 并使用 相同的测试运行器来运行测试。本章介绍 Auto 中可用的测试机制。

为什么测试很重要 ​

_测试_是验证非测试代码是否按预期运行的函数。测试函数通常执行三个操作:

  1. 设置所需的数据或状态。
  2. 运行你要测试的代码。
  3. 断言结果符合你的预期。

编写测试有具体的好处。测试能_及早发现 bug_——当一个测试失败时,你确切知道 哪个行为出了问题。测试让你_自信地重构_——如果更改后所有测试都通过,你很可能 没有破坏任何东西。测试充当_文档_——阅读测试告诉你一个函数应该做什么。

在 Auto 中,测试特别有价值,因为它们在转译后的 Rust 代码上运行。如果你的 Auto 代码能编译通过且所有测试都通过,你就有了强有力的证据证明代码在所有五个 目标平台上都是正确的:Rust、Python、C、TypeScript 和 Auto 本身。

你的第一个测试 ​

Auto 中的测试是用 #[test] 标注的函数。当你运行 automan test 时,转译器 将你的代码编译为 Rust 并运行 cargo test,后者构建一个测试运行器二进制文件 来执行所有标注的函数。

使用 `assert_eq` 的测试函数

#[test] 属性将函数标记为测试。测试运行器找到所有带有此属性的函数并将它们 作为独立的测试用例运行。如果测试函数没有 panic 就返回了,测试通过。如果发生 了 panic(或断言失败),测试失败。

assert_eq 宏检查两个值是否相等。如果不相等,测试会 panic 并显示一条消息, 展示期望值和实际值。这使得调试比简单的 assert 容易得多——你可以确切看到什么 出了问题。

在 Rust 中,assert_eq! 使用 ! 宏后缀并且需要分号。在 Auto 中,assert_eq 不使用 ! 也不需要分号。转译器在生成 Rust 代码时会自动添加。

在 Python 中,assert_eq 转译为普通的 assert result == 4 语句。在 C 中, 它变成带有 fprintf 和 __builtin_trap 的 if 检查。在 TypeScript 中, 它变成抛出 Error 的 if 检查。每个目标平台都使用其惯用的断言机制。

断言宏 ​

Auto 提供三种断言宏,覆盖最常见的测试需求:

  • assert_eq(a, b) —— 如果 a 等于 b 则通过。失败时显示两个值。
  • assert_ne(a, b) —— 如果 a 不等于 b 则通过。失败时显示两个值。
  • assert!(condition) —— 如果 condition 为 true 则通过。失败时不 显示额外上下文。
使用多个断言宏测试字符串操作

这个测试演示了全部三种宏:

  1. assert_eq(s.len(), 5) —— 验证字符串长度恰好为 5。如果字符串是 "hi"(长度 2),测试将失败并显示两个值:assertion failed: left == right, left: 2, right: 5。

  2. assert_ne(s, "world") —— 验证字符串不是 "world"。当你想确认某个值 不同于特定的哨兵值或错误值时,这很有用。

  3. assert!(s.contains("ell")) —— 验证一个布尔条件为 true。这是最通 用的断言——你可以将它用于任何返回布尔值的表达式。如果条件为 false, 测试会以一条通用消息失败(不显示值,因为 assert! 不知道该显示哪些值)。

一个好的做法是尽可能使用 assert_eq 和 assert_ne,因为它们在失败时会显示两 个值。将 assert! 留给值从上下文中显而易见的情况,或者用于测试返回布尔值的 方法(如 .contains())。

在 Python 中,s.contains("ell") 转译为惯用的 in 运算符:"ell" in s。 在 C 中,字符串操作使用 <string.h> 中的 strlen、strcmp 和 strstr。 在 TypeScript 中,.contains() 映射为 .includes()。

测试错误情况 ​

许多函数返回 !T(Result)来表示成功或失败。测试这些函数意味着需要同时测试 成功路径和错误路径。Auto 通过将 !T 与 is 模式匹配结合使用,使这变得很自然。

测试 `!T` 函数的成功和错误路径

让我们分解关键思想:

  1. fn divide(a float, b float) !float —— !float 返回类型表示此函数 返回 Result<float, String>。成功时返回商。失败时(除以零)返回错误 字符串。

  2. if b == 0.0 { "division by zero" } else { a / b } —— 在 Auto 中, 错误返回函数中的裸字符串隐式变为 Err("division by zero")。裸值变为 Ok(a / b)。这是 Auto 简洁的错误处理语法。转译器在 Rust 中将这些展开 为显式的 Ok(...) 和 Err(...) 构造器。

  3. test_divide_by_zero —— 此测试调用 divide(10.0, 0.0) 并对结果进行 模式匹配。如果得到 Ok(_),测试以 assert!(false) 失败——我们期望的 是错误,不是成功。如果得到 Err(e),测试通过。Ok(_) 中的 _ 表示 "我不关心这个值"(因为错误情况下不应该有值)。

  4. test_divide_success —— 此测试调用 divide(10.0, 2.0) 并验证结果。 如果得到 Ok(v),我们使用 assert_eq 检查 v == 5.0。如果得到 Err(_),测试以 assert!(false) 失败——除法本应该成功。

测试 !T 结果的两个分支的模式是基础性的。每个可能失败的函数都应至少有两个 测试:一个用于成功路径,一个用于每个错误路径。

在 Python 中,!T 类型转译为带有 ok 标志的自定义 Result 类。在 C 中, 它变成一个带有布尔判别值的结构体以及 ok 值和错误值的联合。在 TypeScript 中, 它变成带判别值的联合类型,形状为 { ok: true; value: T } 和 { ok: false; error: string }。

集成测试 ​

单元测试在隔离环境中验证单个函数。_集成测试_验证多个组件是否正确协同工作。 它们测试完整的工作流——创建对象、调用多个方法、检查最终状态。

使用 Calculator 类型测试多步工作流

关键观察:

  1. type Calculator { total float } —— 一个带有一个字段的简单类型。total 字段跟踪累加和。

  2. fn new() Calculator —— 一个静态构造函数,创建一个 total 设为 0.0 的 Calculator。在 Auto 中,ext 块内的 fn new() 是静态方法(没有 self 参数)。

  3. fn add(&self, value float) —— 一个修改 self.total 的方法。&self 参数告诉编译器此方法可能修改接收者。在 Rust 中,这转译为 &mut self。 在 Python 和 TypeScript 中,self(或 this)是隐式的。

  4. fn clear(&self) —— 将 total 重置为 0.0。这是另一个修改方法。

  5. 测试 —— 创建一个计算器,添加两个值,检查总和,清除它,然后检查总和 是否回到零。这是一个多步工作流测试。它验证 add 和 clear 单独使用 以及组合使用时都能正确工作。

单元测试和集成测试之间的关键区别在于范围。add 的单元测试可能只测试一次 加法:calc.add(5.0); assert_eq(calc.total, 5.0)。集成测试则练习完整的工作 流:创建、多次添加、检查、清除、再次检查。两者都有价值。

在 C 中,&self 方法变成接受指向结构体的指针(Calculator* self)的函数。 调用者必须显式传递 &calc。在 TypeScript 中,Calculator.new() 是一个返回 新实例的静态工厂方法。

测试组织 ​

随着项目增长,你需要一种组织测试的策略。Auto 遵循与 Rust 相同的约定:单元 测试与被测代码放在一起,集成测试放在单独的 tests/ 目录中。

你还可以使用 spec(来自第 13 章)定义你要测试的接口,并针对该 接口编写测试。这让你可以灵活地替换实现而无需更改测试。

使用规格组织测试

以下是 Auto 中测试组织的工作方式:

  1. spec MathUtils —— 定义契约:任何 MathUtils 的实现都必须提供 add 函数。测试可以针对此规格编写,因此适用于任何实现。

  2. ext MathUtils —— 提供默认实现。add 函数简单地返回 a + b。如果 以后需要一个记录调用或处理溢出的版本,你可以替换实现而无需更改测试。

  3. #[test] fn test_add() —— 测试函数调用 MathUtils.add 并验证结果。 因为测试使用规格名称,所以它适用于任何符合规格的实现。

  4. fn main() —— 测试运行器会忽略 main 函数。只有标记了 #[test] 的 函数在运行 automan test 时才会被执行。

运行测试 ​

运行所有测试:

console
$ automan test

按名称运行单个测试:

console
$ automan test test_add

运行名称匹配模式的测试:

console
$ automan test divide

显示通过测试的输出:

console
$ automan test -- --show-output

一次运行一个测试(用于调试):

console
$ automan test -- --test-threads=1

测试语法快速参考 ​

AutoRust用途
#[test]#[test]将函数标记为测试
assert_eq(a, b)assert_eq!(a, b)断言相等
assert_ne(a, b)assert_ne!(a, b)断言不等
assert!(cond)assert!(cond)断言条件为真
fn test() !fn test() -> Result<(), String>返回错误而非 panic

总结 ​

本章介绍了 Auto 的测试系统。以下是我们学到的内容:

概念语法用途
测试属性#[test]将函数标记为测试
相等断言assert_eq(a, b)验证两个值相等
不等断言assert_ne(a, b)验证两个值不同
布尔断言assert!(cond)验证条件为真
错误测试is result { Ok => ..., Err => ... }测试 !T 的两个路径
运行测试automan test执行所有测试函数

核心要点:

  1. 测试是带 #[test] 的函数 —— 测试运行器发现并执行它们。如果函数没有 panic 就返回了,测试通过。

  2. 优先使用 assert_eq 和 assert_ne —— 它们在失败时显示两个值,使调试 更容易。只有当值不言自明时才使用 assert!。

  3. 测试 !T 的两个路径 —— 每个可能失败的函数都应至少有一个成功路径的 测试和每个错误路径的测试。

  4. 集成测试练习工作流 —— 测试多个组件协同工作:创建、操作、验证、重置、 再次验证。

  5. spec 启用可测试接口 —— 针对规格编写测试,使其适用于任何符合的实现。

  6. automan test 运行一切 —— 它转译为 Rust 并调用 cargo test,给你 Rust 测试运行器的全部功能。

测试不仅仅是为了发现 bug。它是为了对你的代码正确运行有信心——今天如此,未来 每次更改后也是如此。编写测试的小投资会通过减少调试时间和增加重构信心而得到 许多倍的回报。

基于 MIT 许可发布。