第 12 期 | Phase C: 单元测试与 TDD 实战
💡 进群学习加 wx: agentupdate
(申请发送: 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 可以决定:
- 自己修
- 重新 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);
});
});