Rustlings 测试练习精讲:用 assert!、assert_eq! 与 [should_panic] 写出真正能通过的单元测试
Rustlings 的 17_tests 练习组是整套练习中少有的“提前插队”内容:exercises/17_tests/README.md 明确说明,这一章刻意跳出 Rust 官方书籍的章节顺序,先讲测试,因为“后面的许多练习都会要求你让测试通过”(原文:Going out of order from the book to cover tests -- many of the following exercises will ask you to make tests pass!)。本指南以这三道测试练习为主体,完整拆解 assert!、assert_eq! 与 #[should_panic] 三种核心断言机制的用法、取值要求和失败表现,并结合 Rustlings 自身的运行器源码说明“练习通过”的判定标准,帮你把单元测试从会抄变成会写。
一、练习布局:三道题、三套断言机制,以及 Rustlings 如何判定通过
17_tests 目录下的文件构成如下:
| 练习文件 | 考察目标 | 对应参考解 |
|---|---|---|
| exercises/17_tests/tests1.rs | assert! 条件断言 |
solutions/17_tests/tests1.rs |
| exercises/17_tests/tests2.rs | assert_eq! 值相等断言 |
solutions/17_tests/tests2.rs |
| exercises/17_tests/tests3.rs | #[should_panic] 预期 panic 测试 |
solutions/17_tests/tests3.rs |
三道题共享同一个结构:一个普通的 main() 函数(注释写着“You can optionally experiment here”,即可选实验区),外加一个由 #[cfg(test)] 属性修饰的 mod tests 模块。#[cfg(test)] 的含义是:该模块只在 cargo test 编译时才存在,平时运行 cargo run 不会包含它——这正是 Rust 单元测试的标准写法,测试代码与生产代码同文件、同模块树,但物理上被条件编译隔离。
Rustlings 的练习校验也建立在这一机制上。从源码结构看,运行器对每道练习执行的正是 cargo test:src/exercise.rs 中会丢弃一次编译输出、再调用 cargo test … 并检查其退出状态;src/info_file.rs 的注释同样写明了对练习运行 cargo test。因此这三道练习的“通过标准”不是 cargo run 成功,而是:测试模块里的每一个 #[test] 函数都必须编译成功且断言全部通过。这也是为什么三道题都在 mod tests 里留了空参数或 todo!()——留空意味着编译失败或测试失败,练习自然无法过关。
练习注册关系可以在 dev/Cargo.toml 中查证:tests1、tests2、tests3 与其 *_sol 解答版都被声明为该 Cargo 包的 bin target,分别指向 exercises/17_tests/*.rs 和 solutions/17_tests/*.rs。
二、tests1:用 assert! 做条件断言,并用 ! 取反
exercises/17_tests/tests1.rs 的被测对象是一个最简单的函数:
fn is_even(n: i64) -> bool {
n % 2 == 0
}
练习给出的骨架是:
#[cfg(test)]
mod tests {
// TODO: Import `is_even`. You can use a wildcard to import everything in
// the outer module.
#[test]
fn you_can_assert() {
// TODO: Test the function `is_even` with some values.
assert!();
assert!();
}
}
它考察两个知识点:
-
导入被测函数。
mod tests是外部模块的兄弟模块,要用通配符导入外部模块的所有项。solutions/17_tests/tests1.rs 的解法是:// When writing unit tests, it is common to import everything from the outer // module (`super`) using a wildcard. use super::*;super指mod tests的父模块(即文件顶层),use super::*把is_even等符号引入测试模块作用域。这是 Rust 官方书籍推荐、也是本仓库所有测试练习的统一写法。 -
assert!的两种用法。assert!(expr)要求expr求值为true,否则测试失败。参考解里用到了两条断言:assert!(is_even(0)); assert!(!is_even(-1)); // ^ You can assert `false` using the negation operator `!`.- 第一条断言“0 是偶数”直接通过;
- 第二条验证
is_even(-1)为false:由于assert!只能断言真,需要用逻辑非运算符!对函数结果取反,再交给assert!。这正是注释强调的细节——断言一个函数“应该返回 false”,方式是断言其否定。
注意参数选取本身也隐含了边界意识:
0是非负偶数,-1覆盖负数分支,i64取模对负数结果的行为(-1 % 2 == -1,不等于 0)正好被这条用例间接验证。
三、tests2:用 assert_eq! 精确比较函数返回值
exercises/17_tests/tests2.rs 的被测函数利用位移计算 2 的幂:
// Calculates the power of 2 using a bit shift.
// `1 << n` is equivalent to "2 to the power of n".
fn power_of_2(n: u8) -> u64 {
1 << n
}
题目要求在 you_can_assert_eq 中补全 4 条 assert_eq!。与 assert! 只判断布尔不同,assert_eq!(left, right) 直接比较两个值是否相等,失败时会同时打印左右两边的实际值,调试定位更快——这是它比 assert!(a == b) 更常用的原因。solutions/17_tests/tests2.rs 给出的四条用例是:
#[test]
fn you_can_assert_eq() {
assert_eq!(power_of_2(0), 1);
assert_eq!(power_of_2(1), 2);
assert_eq!(power_of_2(2), 4);
assert_eq!(power_of_2(3), 8);
}
从这组用例可以读出两点设计意图:
- 从左到右、逐位翻倍:
1, 2, 4, 8正好是1 << 0到1 << 3的期望值,覆盖了指数的最低几位,任何位运算实现错误(如误写成n << 1)都会立刻暴露; - 类型转换是隐式正确的:
1 << n中1会按返回类型推断为u64,n: u8决定位移量。assert_eq!要求两侧类型一致(都实现PartialEq且类型相同),这里u64 == u64成立;若手误写成assert_eq!(power_of_2(0), 1u32),会直接得到编译错误而非测试失败——类型系统在这里替你兜底。
四、tests3:用 #[should_panic] 测试“会 panic 的代码路径”
exercises/17_tests/tests3.rs 是三道题中最完整的案例,被测对象是一个在非法输入下主动 panic! 的构造函数:
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
// Don't change this function.
fn new(width: i32, height: i32) -> Self {
if width <= 0 || height <= 0 {
// Returning a `Result` would be better here. But we want to learn
// how to test functions that can panic.
panic!("Rectangle width and height must be positive");
}
Rectangle { width, height }
}
}
注释特意点明:现实中返回 Result 是更好的设计(这一点与后续 13_error_handling 练习的主题呼应),但此处刻意保留 panic 版本,目的就是教你测试会 panic 的函数。练习包含三个测试,逐一说明:
1. 正常路径:字段值断言
#[test]
fn correct_width_and_height() {
let rect = Rectangle::new(10, 20);
assert_eq!(todo!(), 10); // Check width
assert_eq!(todo!(), 20); // Check height
}
todo!() 是一个宏,它让程序直接 panic,占位提示“此处尚未实现”。参考解把它替换为结构体字段访问:
let rect = Rectangle::new(10, 20);
assert_eq!(rect.width, 10); // Check width
assert_eq!(rect.height, 20); // Check height
注意这里验证的是构造参数确实被原样存进了字段——这是对 Rectangle { width, height } 这种字段缩写构造的回归测试。另外一个仓库级细节:dev/Cargo.toml 的 clippy lint 配置里写着 todo = "forbid"(注释是 “You forgot a todo!()!”),也就是说忘记把 todo!() 替换掉会直接触发 lint 错误,练习从机制上强迫你补全断言。
2. 预期 panic 路径:#[should_panic]
#[test]
fn negative_width() {
let _rect = Rectangle::new(-10, 10);
}
#[test]
fn negative_height() {
let _rect = Rectangle::new(10, -10);
}
这两个测试的注释都要求“检查负宽/负高时程序是否 panic”。但原样保留它们,测试一定会失败:Rectangle::new(-10, 10) 触发 panic!,而 libtest 默认期望测试函数正常返回,panic 即判失败。
修复方式(见 solutions/17_tests/tests3.rs)是在函数上追加属性:
#[test]
#[should_panic] // Added this attribute to check that the test panics.
fn negative_width() {
let _rect = Rectangle::new(-10, 10);
}
#[test]
#[should_panic] // Added this attribute to check that the test panics.
fn negative_height() {
let _rect = Rectangle::new(10, -10);
}
#[should_panic] 反转了通过标准:测试函数 panic 才算通过,正常返回反而算失败。这样就把“非法输入必须被拒绝”这一行为契约固化成了可执行断言。两个测试分别覆盖宽度为负、高度为负两个分支,与 new 中的 width <= 0 || height <= 0 条件一一对应。
顺带一提,dev/Cargo.toml 对练习包配置了 [profile.dev] 与 [profile.release] 下的 panic = "abort",这属于 Rustlings 对练习运行环境的统一约束(同文件还通过 unsafe_code = "forbid"、unstable_features = "forbid" 等 lint 约束练习代码风格);本练习关注的核心仍是 #[should_panic] 语义本身:为“预期崩溃”的代码路径编写可验证的测试。
五、动手验证:如何确认自己真的做对了
完成修改后,有三层证据链可以自我验证:
- 单独跑该练习的测试。Rustlings 把每道练习注册为独立 bin target(见 dev/Cargo.toml),而 Rustlings 运行器内部就是对该练习执行
cargo test并检查退出码(src/exercise.rs)。你也可以在练习包环境中直接以cargo test tests1/cargo test tests2/cargo test tests3的方式单独筛选运行,观察输出中test result: ok与用例数(tests1/2 各 1 个、tests3 共 3 个); - 观察失败信息。把
assert_eq!(power_of_2(2), 4)故意改成3,cargo test会打印左右值不匹配的详细信息;把#[should_panic]删掉再跑,negative_width会因 panic 而失败——这两类失败信息正是断言机制存在的意义; - Rustlings 的自检验证了这套判定。仓库的集成测试目录 tests/test_exercises/ 内置了
test_success.rs、test_failure.rs、compilation_success.rs、compilation_failure.rs四类最小样例(见 tests/test_exercises/exercises/),配合 tests/integration_tests.rs 验证“测试通过/失败、编译通过/失败”四种状态都能被运行器正确识别——也就是说,你在这三道练习里看到的“通过”信号,与 Rustlings 自身 CI 验证过的判定逻辑是同一套。
六、小结:三道题背后的测试心智模型
assert!(cond):断言布尔条件;要断言“为假”时用!取反(tests1 的!is_even(-1))。assert_eq!(a, b):断言两值相等,失败时打印双方实际值,且要求类型一致(tests2 的四条 2 的幂用例)。#[should_panic]:为“非法输入必须 panic”的行为契约编写测试,反转通过标准(tests3 的负宽/负高用例)。- 配套工程事实:测试代码用
#[cfg(test)]条件编译隔离、用use super::*引入被测项;Rustlings 以cargo test的退出码判定练习成败,并以 clippytodo = "forbid"杜绝占位符残留。
掌握这三点之后,后续练习中“make the tests pass”的要求就不再是谜:先看清 #[test] 函数里的断言在验证什么行为,再修改被测代码或补全断言,使 cargo test 全绿即可。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00