第 12 期 | Phase C: 单元测试与 TDD 实战

⏱ 预计阅读 14 分钟 更新于 2026/5/7
💡 进群学习加 wx: agentupdate
(申请发送: agentupdate)

12.1 Phase C/D: 测试与验收(qa-engineer)

12.1.1 前置条件确认

在 spawn qa-engineer 之前,确认:

flowchart TD
    A["ui-dev Phase A+B 完成?"] -->|"检查 TaskList"| B{"Task #1 #2
completed?"} C["logic-dev Phase A+B 完成?"] -->|"检查 TaskList"| D{"Task #3 #4
completed?"} B -->|Yes| E{"文件存在?"} D -->|Yes| E B -->|No| F["等待或发消息催促"] D -->|No| F E -->|"index.html + style.css
+ app.js 都存在"| G["✅ 可以 spawn qa-engineer"] E -->|"文件缺失"| H["检查 inbox 文件
确认 agent 真实状态"] style G fill:#c8e6c9 style F fill:#fff9c4 style H fill:#ffcdd2

确认命令:

# 检查 Task 状态
# 在 Claude Code 中执行:
TaskList()

# 检查文件
ls -la index.html style.css app.js

# 检查 inbox(如果 TaskList 为空,可能是 session 重置)
cat ~/.claude/teams/calc-dev/inboxes/team-lead.json | python3 -c "
import json, sys
msgs = json.load(sys.stdin)
for m in msgs:
    if '完成' in m.get('text','') or 'completed' in m.get('text','').lower():
        print(f'FROM: {m[\"from\"]}')
        print(f'MSG: {m[\"text\"][:100]}')
        print()
"

12.1.2 为 qa-engineer 准备 tmux Pane

qa-engineer 也需要一个独立的 tmux pane 来运行。有两种方式:

方式 1:复用已释放的 agent pane(推荐)

ui-dev 和 logic-dev 完成后,它们的 tmux pane 会空闲。可以在 setup-team.sh 中预留第 3 个 pane:

# 修改 scripts/setup-team.sh,增加第 3 个 pane
tmux split-window -h -t "$SESSION" -n "qa-engineer"
tmux select-pane -t "$SESSION:0.3" -T "qa-engineer"

方式 2:让 Claude Code 自动创建(实际行为)

Agent 工具 spawn 时,Claude Code 会自动在新的 tmux session 中创建 pane,不需要手动准备。calc-dev session 中的 pane 都是空的 shell,agent 实际运行在自动创建的 session 中。

# spawn qa-engineer 后,查看它实际运行在哪个 session:
tmux list-sessions
# 输出可能类似:
# 0: 1 windows
# 2: 1 windows    ← ui-dev 在这里
# 3: 1 windows    ← qa-engineer 可能在这里
# 4: 1 windows    ← logic-dev 在这里
# calc-dev: 1 windows

# 查看所有 pane 的进程
tmux list-panes -a -F '#{session_name}.#{pane_index} #{pane_current_command}'
# 2.1.118 = Claude Code agent 进程
# zsh = 空 shell

12.1.3 Spawn qa-engineer

确认前置条件后执行:

Agent({
  name: "qa-engineer",
  mode: "bypassPermissions",
  team_name: "calc-dev",
  prompt: `你是 qa-engineer,负责测试和质量保证。

不要调用任何 Skill。直接写代码和测试。
所有代码已由 ui-dev 和 logic-dev 完成。

先读 requirement.md 了解需求。再读 index.html、style.css、app.js 了解代码。

任务 1:Phase C — 单元测试 + 集成测试(Task #5)
- 项目已有 test-runner.js(logic-dev 创建的轻量测试框架),在此基础上扩展
- 单元测试:计算引擎(加减乘除、括号、边界值、除零)
- 单元测试:表达式解析器(非法输入、嵌套括号)
- 单元测试:历史记录(存储上限 10 条、点击回填)
- 集成测试:DOM 交互(按钮点击 → 显示更新)
- 集成测试:键盘事件(完整键盘操作流程)
- 集成测试:模式切换(样式切换、localStorage、按钮显隐)
- 集成测试:老年模式(长按退格、高级运算隐藏)

任务 2:Phase D — E2E 测试与验收(Task #6)
- E2E:完整用户流程(输入 → 计算 → 历史 → 切换模式)
- E2E:老年模式完整流程
- 响应式测试:验证 CSS 媒体查询在不同视口下生效
- 无障碍测试:验证 ARIA 标签、键盘导航全流程
- 发现 bug 能修的直接修,修不了的通知 team-lead
- 对照 requirement.md 逐项验收,列出通过/未通过项

每个任务完成时:
1. TaskUpdate 标记 completed
2. SendMessage 通知 team-lead

Working directory: /Users/eric/work/teamtest`
})

12.1.4 分配 Task 并设置依赖

// 分配 qa-engineer 的任务
TaskUpdate({ taskId: "5", owner: "qa-engineer" })
TaskUpdate({ taskId: "6", owner: "qa-engineer" })

// 依赖关系已在 Phase A 中设置:
// Task #5 blockedBy Task #2 + Task #4(等 ui-dev + logic-dev 的 Phase B)
// Task #6 blockedBy Task #5(等 Phase C 测试完成)

12.1.5 监控 qa-engineer 进度

flowchart TD
    QA["qa-engineer spawn"] --> R1["读取所有源代码"]
    R1 --> T5["Task #5: Phase C
单元测试 + 集成测试"] T5 --> T5A["扩展 test-runner.js"] T5A --> T5B["test.js — 48 单元测试"] T5B --> T5C["test-dom.js — DOM 集成测试"] T5C --> T5D["test-a11y.js — 无障碍测试"] T5D --> T5E["test-runner.html — 浏览器测试页面"] T5E --> T5F["TaskUpdate completed #5"] T5F --> T6["Task #6: Phase D
E2E + 验收"] T6 --> T6A["E2E 完整流程测试"] T6A --> T6B["对照 requirement.md 验收"] T6B --> T6C["TaskUpdate completed #6"] T6C --> NOTIFY["SendMessage 通知 team-lead"] style QA fill:#e1f5fe style NOTIFY fill:#c8e6c9

查看 qa-engineer 运行状态:

# 找到 qa-engineer 的 tmux session
tmux list-panes -a -F '#{session_name}.#{pane_index} #{pane_pid} #{pane_current_command}'

# 切换到 qa-engineer 的 session 查看
tmux switch-client -t <session-name>

# 或者直接在当前 session 查看文件变化
watch -n 5 'ls -la test*.js test*.html 2>/dev/null'

实际产出时间线:

时间 文件 说明
T+2min test.js 单元测试框架 + 核心函数测试
T+5min test-runner.html 浏览器测试运行器
T+8min test-dom.js DOM 交互集成测试
T+10min test-a11y.js 无障碍测试
T+12min test-runner.html 更新 整合所有测试

12.1.6 处理 qa-engineer 的通知

qa-engineer 完成后会 SendMessage 通知 team-lead。但根据前面讨论的通知机制,消息可能不弹窗

# 主动检查 inbox 确认完成
cat ~/.claude/teams/calc-dev/inboxes/team-lead.json | python3 -c "
import json, sys
msgs = json.load(sys.stdin)
for m in msgs:
    if m.get('from','').startswith('qa'):
        print(f'FROM: {m[\"from\"]}')
        print(f'TIME: {m[\"timestamp\"]}')
        print(f'MSG: {m[\"text\"][:200]}')
        print()
"

如果 qa-engineer 发现了 bug:

qa-engineer prompt 中写了"发现 bug 能修的直接修"。如果修不了,它会 SendMessage 给 team-lead 描述问题。team-lead 可以决定:

  1. 自己修
  2. 重新 spawn ui-dev / logic-dev 来修

12.1.7 测试框架(test-runner.js)

qa-engineer 使用 logic-dev 创建的轻量测试框架,无外部依赖:

class TestRunner {
  describe(name, fn) { /* 测试套件 */ }
  it(name, fn) { /* 测试用例 */ }
  assert = {
    equal(actual, expected, msg) { /* ... */ },
    deepEqual(actual, expected, msg) { /* ... */ },
    truthy(value, msg) { /* ... */ },
    throws(fn, msg) { /* ... */ },
    closeTo(actual, expected, delta, msg) { /* ... */ }
  }
  run() { /* 执行所有测试,输出结果 */ }
}

12.1.8 单元测试(test.js)— 48 用例

const { tokenize, parseExpression, evaluate, /* ... */ } = require('./app.js');

describe('tokenize', () => {
  test('数字和运算符', () => expect(tokenize('1+2')).toEqual(['1','+','2']));
  test('含括号', () => expect(tokenize('(1+2)*3')).toEqual(['(','1','+','2',')','*','3']));
  test('空输入', () => expect(tokenize('')).toEqual([]));
});

describe('evaluate', () => {
  test('加法', () => expect(evaluate(parseExpression(tokenize('1+2')))).toBe(3));
  test('除零保护', () => expect(evaluate(parseExpression(tokenize('1/0')))).toBe('Error'));
  test('括号计算', () => expect(evaluate(parseExpression(tokenize('2*(3+1)')))).toBe(8));
  test('小数精度', () => expect(evaluate(parseExpression(tokenize('0.1+0.2')))).toBe(0.3));
});

describe('边界情况', () => {
  test('多重括号嵌套', () => {
    expect(evaluate(parseExpression(tokenize('((1+2)*(3+4))')))).toBe(21);
  });
  test('大数计算', () => {
    expect(evaluate(parseExpression(tokenize('999999*999999')))).toBe(999998000001);
  });
});