ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

第二十九篇:TDD实战:让Claude Code先生成单元测试,再写实现代码

第二十九篇:TDD实战:让Claude Code先生成单元测试,再写实现代码

1. 为什么“先写测试”在 Claude Code 里突然变得顺手了

TDD 这套红-绿-重构的循环,理论很优雅,落地很痛苦。我见过太多团队在 Sprint 计划里写上“本迭代推行 TDD”,两周后测试文件里只剩一个expect(true).toBe(true)。问题不在理念,而在成本:写一个formatPrice函数,手工构造输入、断言输出、模拟依赖,可能比写实现本身还慢。

Claude Code 这类 Agent 工具改变的是这个成本结构。你不再需要逐行敲测试代码,而是用自然语言描述“这个函数应该做什么”,让 AI 先生成 Jest 测试文件,运行、看它失败(红),再生成最小实现让它通过(绿),最后按需重构。你从“写测试的人”变成“定义规范的人”。

这篇文章聚焦一件事:怎么用 Claude Code 在真实项目里跑通测试先行的闭环。我会给出可复制的提示词模板、Jest 配置片段、红绿重构的验证步骤,以及如何通过 TaoToken 统一 Key 和 API 通道接入,让团队里每个人都能用同一套配置。适合已经会写 JavaScript/TypeScript、想用 AI 加速 TDD 但还没跑通流程的开发者。

核心检索词先明确:TDD、Claude Code、单元测试、Jest、测试驱动开发。这几个词会贯穿全文,因为它们是你在搜索和实际落地时真正会敲进终端的东西。

2. 用 TaoToken 统一 Key 与 API 通道接入 Claude Code

在让 Claude Code 帮你写测试之前,得先让它能稳定调用模型。团队协作里最烦的是每个人 Key 不一样、额度分散、换个人就报 401。TaoToken 的做法是提供一个统一的 API 通道,你拿一个 Key,就能在 Claude Code、Cline、Codex 这些工具里复用。

先说清楚它是什么:TaoToken 是一个模型 API 聚合与转发服务,官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。你注册后在控制台生成 Key,把它填进 Claude Code 的配置里,就能走统一通道调用模型。它不替代编辑器,也不碰你的代码仓库,只负责把请求转发到模型侧。

Claude Code 的接入方式是通过环境变量或配置文件指定 Base URL 和 Key。我实测下来,最省事的是在项目根目录或用户目录下配置。下面是一个可复制的配置片段,路径按 Claude Code 的实际约定来:

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的TaoToken密钥" } }

如果你用的是 Claude Code 的 settings 文件,通常放在~/.claude/settings.json或项目级.claude/settings.json。写入后重启 Claude Code,它会读取这个 Base URL,把请求发到 TaoToken 的通道。

这里有个关键点:Base URL 填https://taotoken.net/api,不要带多余的路径后缀。Key 从控制台复制,格式一般是sk-开头。Model ID 方面,Claude Code 默认会用它自己的模型标识,你不需要额外改,除非你想指定特定模型。

如果你同时用 Cline 或 Codex,三件套要写全:Base URL、Key、Model ID。Cline 的 MCP 配置里,Base URL 同样是https://taotoken.net/api,Key 用同一个,Model ID 按你选的模型填。Codex 的auth.json里则是把 API Key 和 Base URL 对应写进去。统一用一个 Key 的好处是:额度集中管理,换工具不用重新申请,团队新人入职只要拿到一个 Key 就能跑起来。

拿 Key 的入口在控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。API Keys 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。这几个链接建议收藏,排障时会反复用到。

配置完成后,你可以先在 Claude Code 里发一句“你好,确认一下连接”,如果它能正常回复,说明通道通了。如果报 401,多半是 Key 复制时带了空格或换行;如果报 local proxy failed,检查 Base URL 是否写成了带/v1的旧格式。这些错误我在第 5 节会逐个拆。

3. 可复制的 Jest 配置与 TDD 提示词模板

这一节是全文最核心的部分,因为你要的是能直接粘贴进项目的东西。先给 Jest 配置,再给提示词模板,最后给一个完整的红绿重构示例。

3.1 Jest 配置片段

在项目根目录建jest.config.js,内容如下:

module.exports = { testEnvironment: 'node', testMatch: ['**/*.test.js'], collectCoverage: true, coverageDirectory: 'coverage', coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } }, verbose: true };

testMatch限定只跑.test.js结尾的文件,避免误扫node_modules。coverageThreshold设 80% 是团队常见的门槛,低于这个值 Jest 会直接失败,这样 CI 里能卡住。verbose: true让每个用例的名字都打印出来,红绿阶段看得清楚。

package.json里加一行脚本:

{ "scripts": { "test": "jest", "test:watch": "jest --watch" } }

test:watch在本地开发时很有用,你让 Claude Code 改代码,它跑一次,你也能在终端看到实时结果。

3.2 TDD 提示词模板

这是让 Claude Code 按 TDD 流程工作的关键。模板要包含四块:函数签名、行为规范、边界条件、执行要求。我常用的模板长这样:

请用 TDD 方式实现函数 formatPrice(price, currency)。 行为规范: - price 是 number,支持正数、负数、零 - currency 是 'USD' 或 'EUR',USD 符号 $,EUR 符号 € - 千位分隔符用逗号,保留两位小数 - 负数显示为 -$1,234.50 - price 不是数字时返回 'Invalid price' 边界条件: - 零值、整数、小数、负数、非数字输入 执行要求: - 测试框架 Jest,测试文件 formatPrice.test.js - 先写测试,运行确认失败(红) - 再写最小实现,运行确认通过(绿) - 最后检查是否需要重构,重构后测试必须仍然通过 - 实现文件 formatPrice.js,用 module.exports 导出

这个模板的要点是“先写测试,运行确认失败”。很多人让 AI 写测试时忘了让它真的跑一遍,结果测试和实现一起生成,红阶段被跳过,TDD 就名存实亡了。明确要求它运行,它才会去执行npm test并观察输出。

3.3 红阶段:生成的测试文件

Claude Code 按模板会先生成formatPrice.test.js:

const formatPrice = require('./formatPrice'); describe('formatPrice', () => { test('formats positive USD correctly', () => { expect(formatPrice(1234.5, 'USD')).toBe('$1,234.50'); }); test('formats positive EUR correctly', () => { expect(formatPrice(1234.5, 'EUR')).toBe('€1,234.50'); }); test('handles zero', () => { expect(formatPrice(0, 'USD')).toBe('$0.00'); }); test('handles negative numbers', () => { expect(formatPrice(-1234.5, 'USD')).toBe('-$1,234.50'); }); test('rounds to two decimals', () => { expect(formatPrice(5.678, 'USD')).toBe('$5.68'); }); test('returns error for non-number price', () => { expect(formatPrice('abc', 'USD')).toBe('Invalid price'); expect(formatPrice(null, 'USD')).toBe('Invalid price'); }); });

此时formatPrice.js还不存在,运行npm test会报Cannot find module './formatPrice',这就是红。Claude Code 看到这个错误后,会进入绿阶段。

3.4 绿阶段:最小实现

function formatPrice(price, currency) { if (typeof price !== 'number' || isNaN(price)) { return 'Invalid price'; } const symbol = currency === 'USD' ? '$' : '€'; const absPrice = Math.abs(price); const formatted = absPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const sign = price < 0 ? '-' : ''; return `${sign}${symbol}${formatted}`; } module.exports = formatPrice;

再跑npm test,全部通过,绿。这个实现刻意保持简单,没有提前抽常量、没有加类型校验,因为 TDD 的原则是“只写让测试通过的最少代码”。重构留到下一步。

3.5 重构阶段

当前实现里symbol的映射和格式化逻辑可以抽出来,但测试没覆盖更多币种,抽了反而增加复杂度。Claude Code 通常会建议保持现状,或者只做很小的可读性调整。如果它提出重构,要求它“重构后重新运行测试”,确认仍然绿。

这套流程跑下来,一个简单函数从描述到测试通过大约 1 分钟。手工写同样的测试和实现,我估计要 10 到 15 分钟。差距主要省在测试用例的构造上。

4. 验证请求与成功结果:从红到绿的完整终端输出

光看代码不够,你得知道终端里实际长什么样,才能判断流程有没有跑对。这一节我把红、绿两个阶段的真实输出贴出来,你对照自己的终端就能定位问题。

4.1 红阶段输出

在formatPrice.js不存在时运行npm test,Jest 会报模块找不到:

FAIL ./formatPrice.test.js ● Test suite failed to run Cannot find module './formatPrice' from 'formatPrice.test.js' 1 | const formatPrice = require('./formatPrice'); | ^ 2 | 3 | describe('formatPrice', () => { Test Suites: 1 failed, 1 total Tests: 0 total

这个输出说明测试文件本身没问题,只是实现缺失。Claude Code 读到Cannot find module后,会生成实现文件。注意:如果它跳过这一步直接生成实现,你要在提示词里强调“先运行测试确认失败”。

4.2 绿阶段输出

实现文件写好后,再跑npm test:

PASS ./formatPrice.test.js formatPrice ✓ formats positive USD correctly (3 ms) ✓ formats positive EUR correctly (1 ms) ✓ handles zero ✓ handles negative numbers ✓ rounds to two decimals ✓ returns error for non-number price Test Suites: 1 passed, 1 total Tests: 6 passed, 6 total Snapshots: 0 total Time: 0.812 s

六个用例全绿,Test Suites: 1 passed。这时候你可以让 Claude Code 继续下一个函数,或者进入重构。

4.3 带 Mock 的异步验证

真实项目里函数往往依赖外部服务。比如getUserEmail(userId)依赖fetchUserFromDB,测试里要 mock 掉数据库调用。Claude Code 生成的测试会这样写:

const { getUserEmail } = require('./getUserEmail'); const { fetchUserFromDB } = require('./db'); jest.mock('./db'); describe('getUserEmail', () => { beforeEach(() => { jest.resetAllMocks(); }); test('returns email when user exists', async () => { fetchUserFromDB.mockResolvedValue({ id: 1, email: 'test@example.com' }); const email = await getUserEmail(1); expect(email).toBe('test@example.com'); expect(fetchUserFromDB).toHaveBeenCalledWith(1); }); test('returns null when user does not exist', async () => { fetchUserFromDB.mockResolvedValue(null); const email = await getUserEmail(999); expect(email).toBeNull(); }); test('handles rejection from fetchUserFromDB', async () => { fetchUserFromDB.mockRejectedValue(new Error('DB error')); await expect(getUserEmail(1)).rejects.toThrow('DB error'); }); });

运行后如果全绿,说明 mock 配置正确。这里有个坑:jest.mock('./db')的路径必须和实现文件里require的路径一致,否则 mock 不生效,测试会去连真实数据库,可能假绿也可能直接超时。Claude Code 一般能处理对,但你要在审查时确认路径。

4.4 覆盖率验证

跑npm test -- --coverage,终端会输出覆盖率表:

-----------------|---------|----------|---------|---------| File | % Stmts | % Branch | % Funcs | % Lines | -----------------|---------|----------|---------|---------| All files | 95.45 | 88.88 | 100 | 95.45 | formatPrice.js | 95.45 | 88.88 | 100 | 95.45 | -----------------|---------|----------|---------|---------|

如果coverageThreshold设了 80%,低于这个值 Jest 会以非零码退出,CI 里就能拦住。这一步是团队推行 TDD 的硬约束,比口头要求有效得多。

5. 本篇常见错误排查:401、local proxy failed、reading choices、OAuth

配置和运行过程中,报错是必然的。这一节把最常见的几类错误和对应处理列出来,你遇到时直接对照。

5.1 401 Unauthorized

API Error: 401 Unauthorized

原因通常是 Key 不对。检查三处:Key 是否从 TaoToken 控制台正确复制(不要带首尾空格)、ANTHROPIC_API_KEY是否写进了正确的配置文件、环境变量是否被其他配置覆盖。如果你在 Claude Code 里同时设了系统环境变量和项目配置,项目配置优先级更高,确认两边一致。

处理方式:重新从 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 复制 Key,粘贴到配置里,重启 Claude Code。

5.2 local proxy failed

Error: local proxy failed to connect

这个错误多半是 Base URL 写错了。正确值是https://taotoken.net/api,不要写成https://taotoken.net/api/v1或带其他后缀。有些旧教程里写的是带/v1的格式,Claude Code 的配置约定不同,多一个路径段就会连不上。

处理方式:把ANTHROPIC_BASE_URL改成https://taotoken.net/api,保存后重启。

5.3 reading choices 相关报错

TypeError: Cannot read properties of undefined (reading 'choices')

这个错误通常出现在响应格式不符合预期时。可能原因是 Model ID 填错,或者请求发到了不兼容的端点。检查你的 Model ID 是否和 TaoToken 文档里列出的可用模型一致。如果你在 Cline 或 Codex 里也遇到,确认三件套(Base URL、Key、Model ID)都写全了。

处理方式:对照 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 里的模型列表,确认 Model ID 拼写正确。

5.4 OAuth 相关报错

OAuth token expired or invalid

Claude Code 某些版本会走 OAuth 流程。如果你用的是 API Key 模式,确保没有残留的 OAuth 配置干扰。检查~/.claude/下是否有旧的 token 文件,必要时清理掉,只保留 API Key 配置。

处理方式:删除旧的 OAuth 缓存文件,重新用 API Key 配置启动。

5.5 测试假绿

PASS ./formatPrice.test.js Tests: 6 passed, 6 total

但函数实际行为不对。这通常是 mock 配置错误或断言写反了。处理方式:让 Claude Code “用真实依赖而不是 mock 再跑一次”,或者手动检查断言逻辑。TDD 的红阶段如果被跳过,测试和实现一起生成,就容易出现这种假绿。所以提示词里“先运行确认失败”这句不能省。

5.6 重构后测试失败

FAIL ./formatPrice.test.js ● formatPrice › handles negative numbers Expected: "-$1,234.50" Received: "$-1,234.50"

重构改变了符号位置。处理方式:让 Claude Code 读取失败信息,修正实现,重新运行直到绿。重构的前提是测试不变,如果测试本身要改,说明重构改变了契约,需要重新评估。

6. 把 TDD 闭环接入团队工作流

单个函数跑通 TDD 只是开始,团队要的是可复制的流程。这一节讲怎么把上面这套东西固化下来。

6.1 在 CLAUDE.md 里预设规则

Claude Code 支持项目级的CLAUDE.md,你可以在里面写死 TDD 规范:

## 测试规范 - 所有新函数必须先用 TDD 方式开发 - 测试框架统一用 Jest - 测试文件命名 *.test.js,与实现文件同目录 - 覆盖率门槛 80%,低于则 CI 失败 - 提示词必须包含“先写测试,运行确认失败,再写实现”

这样团队里每个人用 Claude Code 时,它都会读取这些规则,不用每次重复交代。

6.2 CI 里跑测试

在 GitHub Actions 或 GitLab CI 里加一步:

- name: Run tests run: npm test -- --coverage

配合coverageThreshold,覆盖率不达标直接失败。这样 TDD 生成的测试就成了合并的硬门槛,而不是可选项。

6.3 成本与效率对照

我实测下来,不同复杂度的函数,AI 辅助 TDD 的耗时和成本大致如下:

函数复杂度手工 TDD 时间AI TDD 时间大致成本
简单工具函数10-15 分钟1 分钟内很低
含异步与 mock20-30 分钟2-3 分钟低
复杂算法与多边界1-2 小时10-15 分钟中等

成本取决于你用的模型和 token 量,但相比手工时间,节省是明显的。关键是测试质量取决于你描述的清晰度。复杂业务逻辑建议先让 AI 生成测试框架,你手动补充关键业务断言。

6.4 长期编码用 Coding Plan

如果你打算把 TDD 作为团队的长期开发方式,每天都要跑大量测试生成和验证,可以考虑 TaoToken 的 Coding Plan。入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。它适合持续编码和 Agent 场景,额度更集中,不用每次担心单次调用成本。

6.5 验证模型用模型对话

如果你想先确认某个模型在 TDD 场景下的表现,可以走模型对话入口:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。先聊几句,看它生成的测试用例质量,再决定要不要接入 Claude Code。

6.6 排障与接入文档

遇到配置问题,优先看接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。Key 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。这两个页面覆盖了大部分接入和排障场景。

TDD 在 Claude Code 里跑通的关键,不是 AI 有多聪明,而是你把“先测试后实现”这个顺序用提示词和运行验证固定下来。红阶段必须真的看到失败,绿阶段必须真的看到通过,重构后必须重新跑。这三步做到,测试就不再是负担,而是你改代码时的安全网。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进