rtk 的 Rust TDD 工作流:Red-Green-Refactor 循环与仓库级测试规范解析
本文基于 rtk(Rust Token Killer)仓库中的 Claude Code 技能文档 .claude/skills/rtk-tdd/SKILL.md 展开,系统讲解该项目如何为 Rust 开发强制推行 TDD(Red-Green-Refactor):从"三条 TDD 定律"、逐步的红绿循环命令,到 Rust 惯用测试模式、命名规范、纯 TDD 的适用边界与提交前质量门禁。读完本文,你能把这套完整可执行的 TDD 流程直接套用到 rtk 或任意 Rust 项目的开发中,并理解其背后由 CLAUDE.md、.claude/rules/cli-testing.md 与真实源码共同支撑的工程化闭环。
技能定位:一份自动触发的 TDD 执行手册
rtk-tdd 是 rtk 仓库内置在 .claude/skills/rtk-tdd/SKILL.md 中的 Claude Code 技能(Skill)。其 YAML frontmatter 定义如下:
name: rtk-tdd
description: >
Enforces TDD (Red-Green-Refactor) for Rust development. Auto-triggers on
implementation, testing, refactoring, and bug fixing tasks. Provides
Rust-idiomatic testing patterns with anyhow/thiserror, cfg(test), and
Arrange-Act-Assert workflow.
allowed-tools:
- Read
- Write
- Edit
- Bash
effort: medium
tags: [tdd, testing, rust, red-green-refactor, rtk]
从这份元信息可以读出三个设计意图:
- 自动触发场景明确:实施功能、写测试、重构、修 bug 四类任务命中后,Agent 会按本文档流程执行,而不是自由发挥;
- 工具白名单收敛:
allowed-tools只开放Read、Write、Edit、Bash四项,即"读代码 + 改代码 + 跑测试"的最小权限集; - 执行强度标注:
effort: medium表明该技能按中等强度执行,配合tags中的red-green-refactor让技能体系可被检索归类。
该技能不是孤立存在的:同目录下还有配套的参考文件 .claude/skills/rtk-tdd/references/testing-patterns.md,提供 RTK 仓库专属的测试模式样例与"未测试模块待办清单";仓库根 .claude/skills/tdd-rust/SKILL.md 则是面向"新增过滤器"场景的姊妹技能(多了 fixture 捕获、insta 快照与 token 节省率断言环节)。两套技能一通用一专用,构成本仓库的 TDD 方法论层。
TDD 三条定律:先失败,再最小化
文档开篇即给出本仓库的"三条 TDD 定律",这是整个工作流的宪法性约束:
- 没有失败的测试,不许写生产代码(Do NOT write production code without a failing test);
- 只写刚好能失败的测试(Write only enough test to fail,包括编译失败也算失败);
- 只写刚好能让失败测试通过的生产代码(Write only enough production code to pass the failing test)。
三条定律对应固定的循环:RED(测试失败)→ GREEN(以最小代码通过)→ REFACTOR(清理并全量回归 cargo test)。其中第 2、3 条的"just enough"是关键:测试不追求一次覆盖所有边界,实现不追求一次做到优雅——把复杂度分摊到 REFACTOR 阶段,保证每次提交都只包含一个已验证的行为增量。
Red-Green-Refactor 六步操作
文档将循环展开为可直接执行的六步,每一步都带明确的 cargo 命令与验证条件:
1. 在同一文件的 #[cfg(test)] mod tests 中写测试
2. cargo test MODULE::tests::test_name -- 必须失败(red)
3. 在函数中实现最小逻辑
4. cargo test MODULE::tests::test_name -- 必须通过(green)
5. 按需重构,重跑 cargo test(保持 green)
6. cargo fmt && cargo clippy --all-targets && cargo test (最终门禁)
几个要点值得强调:
- 测试与实现同文件。Rust 惯用做法是把
#[cfg(test)] mod tests放在被测模块的同一.rs文件底部,通过use super::*;直接访问私有函数——这正是 references/testing-patterns.md 中所有样例模板的写法。 - 第 2 步不可跳过。文档原文警告:"Never skip step 2. If the test passes immediately, it tests nothing."——一个不经过红灯阶段的测试,无法证明它在守护新行为,很可能是对既有行为的重复断言。"编译失败也算失败"这一点也很务实:先引用尚不存在的函数(如
filter_xxx)写测试,cargo test直接报编译错误,这就是合法的 RED。 - 第 6 步是最终门禁,对应文档"Pre-Commit Gate"一节(见后文),三条命令全部通过才允许进入下一步或提交。
以 rtk 仓库的实际代码为例,src/core/utils.rs 中的纯函数 truncate 就是这套流程的标准受益者——其上方 doc 注释里直接内嵌了 doctest:
/// use rtk::utils::truncate;
/// assert_eq!(truncate("hello world", 8), "hello...");
/// assert_eq!(truncate("hi", 10), "hi");
这正对应文档中"Pure function (str -> str) → 输入字面量、断言输出"的测试模式(见下节),cargo test 会把 doctest 与单元测试一并执行,属于 GREEN 阶段的最低成本验证。
Rust 惯用测试模式(Idiomatic Patterns)
文档第一张模式表定义了"任何测试的基座 + 常用断言手段":
| 模式 | 用途 | 使用时机 |
|---|---|---|
| Arrange-Act-Assert | 每个测试的基本结构 | 始终 |
assert_eq! / assert! |
直接比较 / 布尔断言 | 确定性值 |
assert!(result.is_err()) |
错误路径测试 | 非法输入 |
Result<()> 返回类型 |
测试中使用 ? 操作符 |
可失败(fallible)函数 |
#[should_panic] |
预期 panic | 不变量、前置条件 |
tempfile::NamedTempFile |
文件/I/O 测试 | 依赖文件系统的代码 |
第二张表则按代码类型给出"测什么、怎么断言"的速查:
| 代码类型 | 测试模式 | 示例 |
|---|---|---|
| 纯函数 (str -> str) | 输入字面量 → 断言输出 | assert_eq!(truncate("hello", 3), "...") |
| 解析/过滤 | 原始字符串 → 过滤 → contains / not-contains | assert!(filter(raw).contains("expected")) |
| 校验/安全 | 边界输入 → 断言 bool | assert!(!is_valid("../etc/passwd")) |
| 错误处理 | 坏输入 → is_err() |
assert!(parse("garbage").is_err()) |
| 结构体/枚举 roundtrip | 构造 → 序列化 → 反序列化 → 比较 | assert_eq!(from_str(to_str(x)), x) |
这些模式与 rtk 仓库的实际测试高度一致。.claude/rules/cli-testing.md 把"过滤器函数测试"列为最高优先级(🔴 Critical),要求测试块与被测过滤器同文件,并对每个新过滤器覆盖"常规情况 + 至少一个边界情况(空输入、错误输出)";其 fixture 策略也印证了模式表中"解析/过滤"一行的落地方式——小型用例用内联字面量,输出较大或格式敏感时用 include_str! 引入 tests/fixtures/ 中的真实命令输出(例如 tests/fixtures/mvn_test_pass_slice_raw.txt、tests/fixtures/gradlew_build_raw.txt),src/cmds/jvm/mvn_cmd.rs 是该 fixture 模式的参考实现。
测试命名规范
文档规定了两类命名模板:
test_{function}_{scenario}
test_{function}_{input_type}
示例:test_truncate_edge_case、test_parse_invalid_input、test_filter_empty_string。
这套命名的实际收益在于:cargo test test_truncate 这类前缀过滤可以精确圈定某函数的全部测试;{scenario} / {input_type} 后缀让"哪个输入、验了什么行为"在测试列表里自解释。配套的 references/testing-patterns.md 还给出了标准骨架模板,约定每个测试模块至少包含 happy_path、empty_input、edge_case 三类用例(边界条件包括超长输入、特殊字符、Unicode)。
何时不该用纯 TDD
文档专设一节划定了纯 TDD 的适用边界——这是很多 TDD 教程容易忽略的部分,直接列出了 rtk 这类 CLI 代理项目中最常见的四类"不适合"场景及处置方式:
- 调用
Command::new()的函数 → 测试解析逻辑(parser),不测进程执行。rtk 的过滤器本质是"执行外部命令 + 压缩输出",执行侧依赖系统环境,解析侧才是纯函数; std::process::exit()→ 先把函数重构为返回Result,再对Result做测试。这与 CLAUDE.md 中"Exit code propagation"规则(子进程失败时透传退出码)是配套的:run()层负责退出码传播,filter_()层保持可测;- 直接 I/O(SQLite、网络) → 用 tempfile / mock,或把纯逻辑拆出来单独测。技能参考文件 references/testing-patterns.md 明确将
tracking.rs(SQLite 统计)列为"需要 tempfile"的中优先级模块,把container.rs、find_cmd.rs等重 I/O 模块列为低优先级; - Main / CLI 装配层 → 交给集成/冒烟测试。对应 CLAUDE.md 中
bash scripts/test-all.sh(冒烟测试)与 .claude/rules/cli-testing.md 中tests/*.rs顶层集成测试(含#[ignore]的真实进程用例)的分工。
从源码结构看,这一边界在 rtk 的代码组织中有清晰投影:src/core/ 与 src/cmds/*/ 中的 filter_*、truncate、is_valid_* 等纯函数是 TDD 主战场,而 main.rs 的 Clap 命令路由与 run() 中的 Command 执行则归入集成测试层。
提交前门禁(Pre-Commit Gate)
文档最后给出三条硬性门禁命令:
cargo fmt --all --check
cargo clippy --all-targets
cargo test
原文要求:"All 3 must pass. No exceptions. No #[allow(...)] without documented justification."——三条全过、无例外,且不允许无书面理由的 #[allow(...)] 豁免。
这套门禁在仓库中有三处呼应:
- CLAUDE.md 的 "Build Verification (Mandatory)":任何 Rust 文件编辑后必须跑完整质量管线,且"fix ALL clippy warnings before moving on(零容忍)";
- .claude/hooks/bash/pre-commit-format.sh:挂在
git commit的 PreToolUse 钩子,自动执行cargo fmt --all,并运行cargo clippy --all-targets——出现error:即阻断提交(警告允许通过); - .claude/rules/cli-testing.md 的 "Testing Checklist":在门禁之外补充了 token 节省率 ≥60% 验证、跨平台 shell 转义测试、
cargo test --ignored集成测试等发布前检查项。
也就是说,SKILL.md 中的"Pre-Commit Gate"不是孤立的口号,而是与钩子脚本、规则文件共同组成的分层防线:技能文档约束 Agent 的行为,钩子脚本约束 git 提交动作,规则文件补充质量维度。
延伸阅读:把技能放回仓库证据链
若要在 rtk 仓库内继续深入,以下路径构成完整的证据链:
- src/core/utils.rs:
truncate、strip_ansi、format_tokens、format_usd等纯函数,每个都带 doctest,是"纯函数 TDD 模式"的现成范本; - references/testing-patterns.md:四类 RTK 专属测试模式(Filter / Pure Computation / Validation / ANSI Stripping)+ 未测试模块待办清单(按可测性分级:纯函数优先、重 I/O 最后);
- .claude/skills/tdd-rust/SKILL.md:面向新增过滤器场景的强化版 TDD 流程——真实 fixture 捕获、
count_tokens节省率断言、insta 快照锁定输出格式; - .claude/rules/cli-testing.md:单元测试 / 集成测试 / 性能测试(<10ms 启动、<5MB 内存)的完整策略与反模式清单;
- tests/fixtures/:真实命令输出的回归夹具库,配合
include_str!使用。
小结
rtk-tdd 技能把 TDD 从"理念"压缩成了一张可机械执行的卡片:三条定律划定行为底线,六步命令给出精确操作序列,两张模式表覆盖 Rust 常见代码类型,命名规范让测试可检索,"何时不适用"一节避免教条主义,三条门禁命令守住提交质量。对于以 Rust 编写的 CLI 工具项目而言,这套"文档即流程、流程即门禁"的 TDD 组织方式——技能文件定义循环、钩子脚本落地拦截、规则文件补齐质量维度——是一个可以直接借鉴的完整参照。
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