第 4 章:核心功能

测试与调试

测试与调试是软件开发的两大支柱,也是 Claude Code 展现其 Agentic 能力最充分的场景之一。不同于传统 IDE 中孤立的代码补全或静态分析,Claude Code 能够在一个连续的代理循环中完成"编写测试 → 运行测试 → 解析失败 → 定位 Bug → 修复代码 → 验证通过"的完整闭环。本章将深入探讨如何利用 Claude Code 进行高效的测试驱动开发、自动化测试生成、迭代式 Bug 修复以及复杂问题的系统性调试。

测试与调试是软件开发的两大支柱,也是 Claude Code 展现其 Agentic 能力最充分的场景之一。不同于传统 IDE 中孤立的代码补全或静态分析,Claude Code 能够在一个连续的代理循环中完成"编写测试 → 运行测试 → 解析失败 → 定位 Bug → 修复代码 → 验证通过"的完整闭环。本章将深入探讨如何利用 Claude Code 进行高效的测试驱动开发、自动化测试生成、迭代式 Bug 修复以及复杂问题的系统性调试。

1. 运行测试套件

Claude Code 通过 Bash 工具执行测试命令,并通过读取输出结果来理解测试状态。它能够识别主流测试框架的输出格式,并据此判断测试通过或失败。

1.1 支持的测试框架

Claude Code 通过 Bash 工具调用测试命令,然后将 stdout/stderr 输出喂给 LLM 解析。由于 LLM 具备通用文本理解能力,它能够解析大多数测试框架的输出格式,但这不是框架级别的原生集成。以下列出常见测试框架及其典型命令:

框架语言/生态典型命令
JestJavaScript/Node.jsnpm test / npx jest
VitestJavaScript/Node.jsnpx vitest
pytestPythonpytest -v
Go TestGogo test ./...
JUnitJavamvn test / ./gradlew test
RSpecRubybundle exec rspec
PlaywrightE2E (JS/TS)npx playwright test
CypressE2E (JS/TS)npx cypress run
REST AssuredAPI (Java)mvn test

Claude Code 在生成测试时会自动检测项目中已有的测试框架和配置。它通过读取 package.jsonpom.xmlCargo.tomlgo.mod 等配置文件,以及扫描现有的测试文件来确定应使用的框架和风格。

1.2 要求 Claude 运行测试

你可以用自然语言要求 Claude 运行测试,它会根据项目结构推断正确的命令。

对话示例

用户: 运行所有测试

Claude: 我来运行项目中的所有测试。

Bash: npm test

测试输出:
> vitest run

 Test Files  12 passed (12)
      Tests  47 passed (47)
   Start at  10:23:45

所有 47 个测试全部通过。

更具体的指令

用户: 只运行 src/services/payment.ts 相关的测试,并显示覆盖率

Claude: 我来运行相关测试并生成覆盖率报告。

Bash: npx vitest run src/services/payment.test.ts --coverage

1.3 测试工作区配置

为了获得最佳的测试体验,建议在开发环境中配置多标签页工作区:

  • 标签页 1: Claude Code 终端 —— 编写提示和生成测试
  • 标签页 2: 测试运行器(watch 模式)—— npm test -- --watch,实时查看测试执行
  • 标签页 3: 应用服务器 —— 保持应用运行以便进行集成验证
  • 标签页 4: 覆盖率报告 —— npm run test:coverage,追踪覆盖进度

这种配置让你能够在 Claude 编写测试的同时,实时看到测试出现并通过的反馈循环。

2. 读取错误输出

当测试失败时,Claude Code 的核心价值在于它能够理解错误输出的语义,而不仅仅是文本匹配。它会分析失败信息、堆栈跟踪和上下文,从而定位问题根因。

2.1 解析测试失败信息

Claude Code 能够理解多种类型的测试失败:

断言失败

expect(received).toBe(expected)

Expected: 42
Received: 38

Claude 会识别期望值与实际值的差异,并追溯计算逻辑。

异常抛出

TypeError: Cannot read property 'name' of undefined
    at UserService.getUserProfile (src/services/user.ts:45:12)

Claude 会沿着堆栈跟踪定位到具体代码行,分析变量为何为 undefined

异步超时

Timeout - Async callback was not invoked within the 5000 ms timeout

Claude 会检查异步操作的实现,识别是否缺少 await、Promise 未正确返回,或存在死锁。

快照不匹配(Jest/Vitest):

Snapshot name: `Button renders correctly 1`
Received value does not match stored snapshot

Claude 会分析 UI 变更是有意的设计修改还是意外的回归,并决定更新快照或修复组件。

2.2 交互式调试循环

最有效的调试方式是将 Claude 作为实时调试伙伴,通过持续对话缩小问题范围:

用户: 测试失败了,帮我看看

Claude: 我来看看测试失败的原因。

[读取测试输出]

失败的是 UserService.create 测试,错误是 "unique constraint violation"。
让我检查一下测试代码和数据库约束。

[读取测试文件和模型定义]

我发现问题:测试之间共享了数据库状态,上一个测试创建的用户
没有清理,导致唯一键冲突。我来修复测试的 setUp/tearDown。

Edit: tests/services/user.test.ts
...

高级技巧:将终端输出通过管道直接喂给 Claude:

npm run test 2>&1 | tee test-output.txt | claude -p "分析这些测试失败"

3. 自动修复失败测试

Claude Code 最强大的能力之一是迭代修复循环(Iterative Fix Loop)。当测试失败时,Claude 可以自主地分析失败原因、修改代码、重新运行测试,直到所有测试通过。

3.1 迭代修复循环

这个循环遵循以下模式:

运行测试 → 发现失败 → 分析原因 → 修复代码 → 重新运行 → 验证通过
   ↑                                                        |
   └──────────────── 仍有失败 ──────────────────────────────┘

对话示例

用户: 实现 calculateDiscount 函数,要求:
  - 接受价格和折扣百分比
  - 处理负值和超过 100% 的边界情况
  - 结果保留两位小数
  - 先写测试,再写实现

Claude: 好的,我按照 TDD 流程来实现。

[步骤 1] 编写失败的测试
Write: src/utils/discount.test.ts

[步骤 2] 运行测试(确认失败)
Bash: npx vitest run src/utils/discount.test.ts
→ 测试失败(函数不存在)

[步骤 3] 编写最小实现
Write: src/utils/discount.ts

[步骤 4] 重新运行测试
Bash: npx vitest run src/utils/discount.test.ts
→ 测试通过

[步骤 5] 重构
Edit: src/utils/discount.ts(优化代码可读性)

[步骤 6] 最终验证
Bash: npx vitest run src/utils/discount.test.ts
→ 全部通过

calculateDiscount 函数已实现,所有测试通过。

3.2 使用 Hooks 实现自动化 TDD

通过配置 .claude/settings.json 中的 Hooks,可以实现真正的自动化 TDD 反馈循环:

每次编辑后自动运行测试

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hook": "npm test -- --bail --watchAll=false 2>&1 | tail -30"
      }
    ]
  }
}

配置后,每次 Claude 写入或编辑文件,测试套件都会自动运行。如果测试失败,Claude 会立即看到输出并修复问题。

强制测试通过才能结束

{
  "hooks": {
    "Stop": [
      {
        "hook": "npm test -- --bail 2>&1 | tail -30"
      }
    ]
  }
}

这个配置确保 Claude 在每次回合结束时运行测试,如果失败则继续修复,形成自我纠正的开发循环。

3.3 完整的 TDD Hook 配置

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "检查是否在编写功能代码。如果是,确认对应的测试文件已存在。如果不存在,提醒先写测试。返回 'approve' 或提醒信息。"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npm test -- --passWithNoTests --watchAll=false"
            // onFailure 字段的可用性请以官方文档为准
            // https://code.claude.com/docs/en/hooks
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "npm test"
            // onFailure 字段的可用性请以官方文档为准
            // https://code.claude.com/docs/en/hooks
          }
        ]
      }
    ]
  }
}

这个配置实现了三层保障:

  1. PreToolUse:在编写功能代码前检查测试是否存在
  2. PostToolUse:每次编辑后自动运行测试
  3. Stop:回合结束时强制测试通过,否则阻塞

4. 调试复杂 Bug

对于复杂的 Bug,Claude Code 能够执行系统性的根因分析,而不是仅仅处理表面症状。

4.1 系统性调试流程

步骤 1:描述症状

用户: 用户报告结账时偶尔看到错误的订单总额。
问题间歇性出现,难以稳定复现。订单确认页面的
总金额与商品加税费和运费之和不符。

步骤 2:分析代码路径

Claude 会追踪涉及的代码路径:

  • 购物车服务(管理商品)
  • 定价服务(计算价格、促销)
  • 税费计算服务
  • 运费服务
  • 结账服务(协调各组件)

步骤 3:识别根因

Claude: 我分析了结账服务的代码,发现了一个关键问题:
结账服务并行调用定价、税费和运费服务,但没有确保
它们都操作同一版本的购物车数据。如果用户在服务调用之间
修改了购物车(添加/删除商品),每个服务可能接收到不同的购物车数据。

这是典型的竞态条件(Race Condition)。

步骤 4:创建复现测试

Claude 会编写一个模拟并发操作的测试来稳定复现问题:

describe('checkout race condition', () => {
  test('should handle concurrent cart modifications during checkout', async () => {
    const cart = await createCartWithItems([
      { productId: 'A', price: 100 },
      { productId: 'B', price: 200 }
    ]);

    // 启动结账流程
    const checkoutPromise = checkoutService.process(cart.id);

    // 在结账过程中并发修改购物车
    await cartService.addItem(cart.id, { productId: 'C', price: 300 });

    const order = await checkoutPromise;

    // 验证订单总额基于结账开始时的购物车快照
    expect(order.total).toBe(300); // 100 + 200,不包含 C
  });
});

步骤 5:实施修复

Claude: 解决方案是在结账开始时创建购物车的不可变快照,
然后将这个快照传递给所有后续服务调用,确保一致性。

Edit: src/services/checkout.ts
// 在结账开始时创建深度不可变快照(生产环境应使用结构化克隆或 immer 等库)
const cartSnapshot = structuredClone(cart);
Object.freeze(cartSnapshot);
// 注意:Object.freeze 是浅冻结,嵌套对象仍需递归处理

// 所有服务使用同一个快照
const [pricing, tax, shipping] = await Promise.all([
  pricingService.calculate(cartSnapshot),
  taxService.calculate(cartSnapshot),
  shippingService.calculate(cartSnapshot)
]);

步骤 6:验证修复

Claude 会运行完整的测试套件,包括新添加的竞态条件测试,确保修复有效且没有引入回归。

4.2 调试场景示例

场景 A:内存泄漏

用户: 应用运行一段时间后内存持续增长,最终 OOM。

Claude: 我来系统性地分析这个问题。

[步骤 1] 检查事件监听器是否未移除
[步骤 2] 检查闭包是否持有大对象引用
[步骤 3] 检查定时器是否未清理
[步骤 4] 检查缓存是否有上限

发现:UserProfile 组件在卸载时没有移除 WebSocket 监听器,
导致每个组件实例都被持续引用。

Edit: src/components/UserProfile.vue
在 onUnmounted 钩子中移除监听器。

场景 B:性能瓶颈

用户: 列表页加载超过 5 秒,数据库查询慢。

Claude: 让我分析查询性能。

[步骤 1] 检查 N+1 查询问题
[步骤 2] 检查是否缺少索引
[步骤 3] 检查是否加载了不必要的关联数据

发现:ProductRepository.findAll() 对每个产品都单独查询了
分类信息,产生 N+1 问题。建议使用 JOIN 或批量查询。

Edit: src/repositories/product.ts
添加 .leftJoinAndSelect('product.category', 'category')

场景 C:仅生产环境出现的 Bug

用户: 这个 Bug 只在生产环境出现,本地无法复现。

Claude: 让我分析日志和环境差异。

[步骤 1] 检查环境变量差异
[步骤 2] 检查数据库数据差异(边界数据、空值)
[步骤 3] 检查第三方服务行为差异
[步骤 4] 检查并发负载差异

建议:在代码中添加结构化日志,记录关键变量状态。
然后分析生产日志中的异常模式。

5. 测试生成

Claude Code 能够为现有代码生成高质量的测试,覆盖正常路径、边界情况和错误条件。

5.1 为现有代码生成测试

单个文件

用户: 为 src/utils/validation.ts 编写全面的测试。
覆盖所有导出函数、边界情况和错误条件。
使用项目中已有的测试约定。

整个模块

用户: 为 src/api/ 目录下的所有模块添加测试覆盖。
参考现有测试的风格。每创建一个测试文件就运行一次,确保通过。

基于产品规格

用户: 这是购物车的产品规格:
- 用户可以添加/删除商品
- 数量更新重新计算总额
- 折扣码支持百分比或固定金额
- 税费基于配送州计算
- 满 $50 免运费

针对 CartService 类编写验证所有这些行为的测试。

5.2 测试生成质量

Claude Code 在理想条件下生成的测试通常具有以下特点:

  • 遵循 Arrange-Act-Assert 模式:结构清晰,可读性强
  • 使用工厂函数createMockUser() 而非硬编码内联数据
  • 覆盖边界情况:null、空数组、边界值、并发访问
  • 描述性命名:测试名称本身就是行为文档
  • 框架原生风格:自动匹配项目使用的测试框架习惯

但实际质量取决于提示词清晰度、上下文完整性和模型版本。AI 生成质量受 prompt 质量、上下文、模型版本影响巨大,不应假设每次都能达到理想输出。

对话示例

用户: 给这个函数写测试

function parseDate(input: string): Date {
  const parts = input.split('-');
  return new Date(+parts[0], +parts[1] - 1, +parts[2]);
}

Claude: 我来为 parseDate 编写全面的测试。

Write: src/utils/date.test.ts

import { describe, test, expect } from 'vitest';
import { parseDate } from './date';

describe('parseDate', () => {
  test('should parse valid ISO date string', () => {
    const result = parseDate('2026-04-26');
    expect(result).toEqual(new Date(2026, 3, 26));
  });

  test('should handle month boundary (January)', () => {
    const result = parseDate('2026-01-01');
    expect(result.getMonth()).toBe(0);
  });

  test('should handle month boundary (December)', () => {
    const result = parseDate('2026-12-31');
    expect(result.getMonth()).toBe(11);
  });

  test('should handle leap year date', () => {
    const result = parseDate('2024-02-29');
    expect(result.getDate()).toBe(29);
  });

  test('should handle empty string', () => {
    expect(() => parseDate('')).toThrow();
  });

  test('should handle malformed input', () => {
    expect(() => parseDate('not-a-date')).toThrow();
  });

  test('should handle single-digit month/day', () => {
    const result = parseDate('2026-1-5');
    expect(result).toEqual(new Date(2026, 0, 5));
  });
});

5.3 从 0% 到 80% 覆盖率策略

对于没有测试的代码库,采用以下策略:

  1. 识别关键模块:让 Claude 分析代码库,识别风险最高的模块(业务逻辑最复杂、依赖最多、Bug 历史最多)
  2. 优先生成关键测试:从 ROI 最高的模块开始
  3. 运行覆盖率报告:检查覆盖缺口
  4. 针对性补充为 UserService 中未覆盖的分支编写测试
  5. 迭代直到达标:每轮速度会越来越快

6. 测试策略:单元、集成、端到端

Claude Code 能够在测试金字塔的每一层发挥作用,关键在于明确各层的职责和协作方式。

6.1 测试金字塔与 Claude 的协作

      /\
     /  \      E2E 测试(少量)—— 验证完整用户旅程
    /----\
   /      \    集成测试(中等)—— 验证组件协作
  /--------\
 /          \  单元测试(大量)—— 验证业务逻辑

单元测试(最多):

  • 覆盖所有公共方法、边界情况和错误条件
  • 外部依赖使用 Mock
  • 执行最快,反馈最及时
  • Claude 可在几分钟内为整个模块生成

集成测试(中等):

  • 验证多个组件协作
  • 使用真实数据库(测试数据库),Mock 外部服务
  • 测试数据流和交互边界
  • Claude 能读取多个源文件理解依赖关系

端到端测试(少量):

  • 模拟真实用户行为
  • 使用 Playwright、Cypress 等工具
  • 覆盖关键用户旅程(注册 → 登录 → 核心功能)
  • Claude 生成 Page Object 模式,测试读起来像用户故事

6.2 各层测试示例

单元测试 —— React 组件

用户: 为 SearchBar 组件写测试,使用 React Testing Library。
测试:默认 props 渲染、用户输入触发防抖搜索、加载状态显示、
结果正确显示、空状态消息。

集成测试 —— API 端点

用户: 为 /api/orders 端点编写集成测试。
测试所有 CRUD 操作、认证要求、验证错误和分页。
使用 supertest。Mock 支付网关,但使用真实数据库。

E2E 测试 —— 用户注册流程

用户: 用 Playwright 编写用户注册流程的 E2E 测试:
注册 → 邮箱验证 → 首次登录 → 新手引导。

6.3 测试数据工厂

Claude Code 倾向于生成可维护的测试数据工厂,而非脆弱的硬编码数据:

// tests/factories/user.factory.ts
let counter = 0;

export function createUser(overrides: Partial<User> = {}): User {
  counter++;
  return {
    id: `user-${counter}`,
    email: `user${counter}@example.com`,
    name: `Test User ${counter}`,
    createdAt: new Date('2026-01-01'),
    ...overrides,
  };
}

7. 调试技巧

7.1 日志分析

当问题难以定位时,让 Claude 在关键路径添加结构化日志:

用户: 这个请求偶尔返回 500,但错误信息不明确。
帮我在关键路径添加日志,追踪请求处理流程。

Claude: 我会在请求处理的关键节点添加结构化日志。

Edit: src/middleware/requestLogger.ts
添加:请求入口、认证结果、数据库查询前后、响应发送

Edit: src/services/orderService.ts
添加:订单创建参数、价格计算中间值、外部 API 调用结果

然后将日志输出持续喂给 Claude 分析:

npm run dev 2>&1 | tee app.log | claude -p "监控并报告任何异常模式"

7.2 二分排查(Git Bisect)

当 Bug 是近期引入的,使用 Git 二分法快速定位:

用户: 这个 Bug 上周还不存在,帮我找出引入它的提交。

Claude: 我来使用 git bisect 定位问题提交。

Bash: git bisect start
Bash: git bisect bad HEAD        # 当前版本有 bug
Bash: git bisect good HEAD~20  # 20 个提交前正常
Bash: git bisect run npm test -- tests/failing.test.ts

[分析结果]

问题提交是 a1b2c3d:"优化查询性能"
该提交移除了一个必要的索引,导致特定查询超时。

7.3 代码审查式调试

让 Claude 以代码审查的视角主动发现潜在问题:

用户: 审查这段代码,找出可能导致 Bug 的模式。

Claude: 我发现了以下潜在问题:

1. [高] 第 45 行:未处理 Promise rejection,可能导致未捕获异常
2. [中] 第 62 行:使用 == 而非 ===,类型强制可能导致意外行为
3. [中] 第 78 行:循环中修改数组长度,可能导致索引跳过
4. [低] 第 91 行:缺少输入验证,依赖调用方提供正确格式

7.4 使用扩展思考模式(Extended Thinking)

对于复杂的架构级 Bug,可启用 Claude 的 Extended Thinking 模式(在支持该功能的模型中,Claude 会在响应前进行更深入的推理)。注意:早期社区流传的 ultrathink 关键字已被官方 Extended Thinking 机制取代。

用户: 分析这个分布式系统中的数据不一致问题。
涉及三个服务和最终一致性。

8. CLAUDE.md 测试配置模板

在项目的 CLAUDE.md 中添加测试规范,确保 Claude 生成的测试始终符合团队标准:

## 测试规范

### 框架
- 单元测试:Vitest
- 组件测试:React Testing Library + Vitest
- E2E 测试:Playwright
- API 测试:supertest

### 约定
- 单元测试与源码同目录(Button.test.tsx 紧邻 Button.tsx)
- E2E 测试放在 tests/e2e/
- 使用工厂函数生成测试数据(见 tests/factories/)
- 用 msw Mock 外部 API(见 tests/mocks/)
- 每个测试文件应可独立运行

### 命令
- `npm test` —— 运行所有单元测试
- `npm run test:e2e` —— 运行 Playwright 测试
- `npm run test:coverage` —— 生成覆盖率报告
- `npm run test:watch` —— watch 模式

### 覆盖率阈值
- 分支覆盖率:80%
- 语句覆盖率:85%
- CI 低于阈值则阻断 PR

### TDD 规则
1. 新功能必须先写失败的测试
2. 只写刚好能通过测试的最小代码
3. 每次绿灯后考虑重构
4. 禁止无测试的功能代码

9. 高级实践:自主测试循环

社区中有开发者提出类似"自主测试循环"的方法论,让 Claude 在真实浏览器中自动执行测试计划、发现 Bug 并修复的迭代流程。这类方法依赖第三方脚本和工作流设计,非 Claude Code 官方功能。

9.1 自主测试循环的核心思想

自主测试循环通常通过以下文件实现:

  • prepare.md —— 指导从测试计划生成状态文件
  • prompt.md —— 每次迭代执行的循环指令
  • status.json —— 追踪所有测试用例的状态
  • results.md —— 人类可读的操作日志

9.2 循环执行流程

每次迭代 Claude 执行:

  1. 读取状态 —— 从 status.json 了解当前测试进展
  2. 选择测试用例 —— 按优先级:进行中 → 失败(尝试 < 3 次)→ 未测试
  3. 执行测试 —— 在浏览器中导航、点击、填写、验证
  4. 记录结果 —— 更新 status.jsonresults.md
  5. 处理失败 —— 分析根因、修复代码、下次迭代重测
  6. 检查完成 —— 全部通过则标记完成

9.3 注意事项

自主测试循环需要精心设计提示词和状态管理机制。由于 Claude Code 本身不提供内置的自动化测试循环命令,这类工作流本质上是利用 Claude 的代理能力配合外部脚本实现的。建议从小范围测试开始验证可行性,再逐步扩展覆盖范围。

10. 最佳实践与注意事项

10.1 有效实践

  • 明确指定测试框架:在 CLAUDE.md 中声明 "使用 Vitest 做单元测试,Playwright 做 E2E",避免 Claude 猜测
  • 指向现有测试作为风格模板"遵循 tests/services/auth.test.ts 的相同模式"
  • 显式要求边界情况"包含 null 输入、空字符串、超大数值和并发访问的测试"
  • 增量运行测试"为每个函数编写测试,边写边运行",尽早发现问题
  • 审查生成的测试:AI 生成的测试可能有盲点 —— 确保它们测试行为而非实现细节

10.2 常见陷阱

  • 测试实现而非行为:如果测试过度依赖内部实现细节,重构时测试会频繁失败
  • 虚假安全感:测试全部通过不代表产品正确工作,代码可能从未被调用(如 WooCommerce 案例中新代码存在但定价页仍调用旧 Stripe 函数)
  • 死重测试:部分 AI 生成测试可能对覆盖率无独立贡献(死重测试),需要人工审查并删除冗余
  • 遗漏关键路径:变异测试显示 AI 可能遗漏 HTTP 500 路径、空集合响应和边界条件

10.3 验证清单

在信任 Claude 生成的测试前,确认:

  • 测试是否真的能失败?(修改代码后测试是否报错)
  • 测试覆盖了多少变异?(使用 mutation testing 验证)
  • 是否有测试是死重?(移除后覆盖率是否不变)
  • 端到端流程是否真正连通?(不仅是单元测试通过)
  • 错误路径是否被覆盖?(不仅是正常路径)

参考来源