ECC 的 TDD 工作流 Skill 实战指南:从 RED-GREEN-REFACTOR 到 80%+ 覆盖率的质量闭环
TDD(测试驱动开发)的难点从来不是"先写测试"这句话本身,而是如何把它变成一条可执行、可验证、可追溯的开发流水线。本文以 ECC(agent harness performance optimization system)仓库中的 tdd-workflow Skill 为核心,系统讲解它在 ECC 中的激活时机、核心原则、完整工作流步骤、单元/集成/E2E 测试模式、Mock 策略与覆盖率门槛,并结合仓库中配套的 tdd-guide Agent、tdd-reminder Hook、quality-gate 质量门禁脚本 以及 测试配置 等源码级佐证,帮你把 TDD 落地为"写新功能、修 Bug、重构"时的默认工作方式。读完本文,你将掌握一套可直接复制的 Red-Green-Refactor 实操流程与测试文件组织规范。
Skill 定位:ECC 中 TDD 的"强制执行器"
在 ECC 的技能体系中,tdd-workflow 是一个描述性 Skill:它本身不执行代码,而是为 Agent(如 Claude Code、Codex、Cursor 中的编码代理)提供"何时激活、遵循什么原则、按什么步骤走"的行为契约。它声明的主要激活时机与用途(见 Skill 源文件):
- 编写新功能(Writing new features or functionality)
- 修复 Bug 或缺陷(Fixing bugs or issues)
- 重构既有代码(Refactoring existing code)
- 新增 API 端点(Adding API endpoints)
- 创建新组件(Creating new components)
ECC 并非只靠这一份 Skill 来推行 TDD,而是形成了"Skill(流程)+ Agent(专家)+ Hook(触发器)+ Steering(约束)+ 脚本(门禁)"的多层配合体系:
| 组件 | 仓库位置 | 职责 |
|---|---|---|
| tdd-workflow Skill | .kiro/skills/tdd-workflow/SKILL.md(增强版见 skills/tdd-workflow/SKILL.md) | 定义完整 TDD 流程、测试模式与覆盖率要求 |
| tdd-guide Agent | .kiro/agents/tdd-guide.md | 扮演 TDD 专家,强制执行 test-first、覆盖 80%+、覆盖边界用例清单 |
| tdd-reminder Hook | .kiro/hooks/tdd-reminder.kiro.hook | 在新建 .ts/.tsx 文件时自动提醒补充测试 |
| 测试约束文档 | .kiro/steering/testing.md | 将"80% 覆盖率 + TDD + 三类测试"固化为 auto inclusion 的全局要求 |
| quality-gate Hook/脚本 | .kiro/hooks/quality-gate.kiro.hook、.kiro/scripts/quality-gate.sh | 一键执行 build / typecheck / lint / tests 四项门禁 |
从源码结构可以推断,这种"Skill 描述流程、Agent 执行监督、Hook 自动触发、脚本实施门禁"的配套设计,目的是让 TDD 从提示词层面的软约束升级为可被自动验证的硬门禁。
核心原则:Test First、80%+ 覆盖、三类测试齐备
Skill 在 Core Principles 一节中确立了三条不可妥协的原则,也是整个流程的"宪法":
- 测试先于代码(Tests BEFORE Code):永远先写测试,再写让测试通过的实现。
- 覆盖率要求(Coverage Requirements):最低 80% 覆盖率(单元 + 集成 + E2E 合计评估);所有边界情况覆盖;错误场景必须被测试;边界条件需被验证。
- 测试类型(Test Types)三足鼎立,缺一不可:
- 单元测试(Unit Tests)——针对单个函数与工具方法、组件逻辑、纯函数、helpers 工具。目标是隔离验证每一块"最小逻辑单元"。
- 集成测试(Integration Tests)——覆盖 API 端点、数据库操作、服务间交互、外部 API 调用。验证模块拼接后的真实协作。
- E2E 测试(Playwright)——覆盖关键用户流程、完整工作流、浏览器自动化、UI 交互。从真实用户视角验证系统整体行为。
ECC 官方仓库自己的测试工程正好印证了这套分层:根目录 tests 下按 tests/lib、tests/hooks、tests/commands、tests/scripts、tests/ci、tests/integration、tests/docs 等子目录组织数百个 JS 测试,而 package.json 中的 "test" 脚本(package.json)会把 Unicode 安全校验、agents/commands/rules/skills/hooks/install-manifests 校验、目录树校验与 node tests/run-all.js 串联执行——单元与集成测试同时覆盖脚本层与规则层,正是"测试不是可选项"思想的仓库级实践。
标准 TDD 工作流:从用户旅程到覆盖率验证
Skill 给出了一条七步流水线,每一步都有明确的产出物与验收标准:
Step 1:编写用户旅程(User Journeys)
先用自然语言把"谁、想做什么、为了什么好处"讲清楚,再据此推导测试:
As a [role], I want to [action], so that [benefit]
Example:
As a user, I want to search for markets semantically,
so that I can find relevant markets even without exact keywords.
用户旅程是需求到测试之间的桥梁:它把模糊的产品诉求翻译成可验证的行为描述。
Step 2:为每个用户旅程生成测试用例
一个用户旅程要拆出多个测试用例,至少包含主路径、边界/空值、降级行为、排序/逻辑四类(示例为 Skill 原文中的语义搜索用例骨架):
describe('Semantic Search', () => {
it('returns relevant markets for query', async () => {
// Test implementation
})
it('handles empty query gracefully', async () => {
// Test edge case
})
it('falls back to substring search when Redis unavailable', async () => {
// Test fallback behavior
})
it('sorts results by similarity score', async () => {
// Test sorting logic
})
})
Step 3:运行测试——确认它们失败(RED)
npm test
# Tests should fail - we haven't implemented yet
这一"红"必须被真实观察到才算数:一个只写出来、从未编译运行过的测试不构成 RED 证据。只有当失败源于预期的业务逻辑缺陷、未定义行为或缺失实现(而非测试脚手架本身坏了)时,RED 门禁才有效;在 RED 被确认之前,不允许改动生产代码。
Step 4:实现代码(GREEN 前半)
写最少量的代码让测试通过,实现完全由测试牵引:
// Implementation guided by tests
export async function searchMarkets(query: string) {
// Implementation here
}
Step 5:再次运行测试——确认全部通过(GREEN)
npm test
# Tests should now pass
Step 6:重构(REFACTOR)
在测试保持绿色的前提下提升代码质量:
- 消除重复(Remove duplication)
- 改进命名(Improve naming)
- 优化性能(Optimize performance)
- 增强可读性(Enhance readability)
Step 7:验证覆盖率
npm run test:coverage
# Verify 80%+ coverage achieved
在 ECC 仓库自身的脚本体系中,这一步同样有据可查:package.json 的 "coverage" 脚本(package.json)使用 c8 对 scripts/**/*.js、scripts/**/*.mjs 强制 --lines 80 --functions 80 --branches 79 --statements 80 的阈值并输出 text/lcov 报告——把"80%+"的 Skill 要求落实成了 CI 层面会直接失败的具体配置。
进阶增强:运行时检测与证据链(取自增强版 Skill)
ECC 仓库中与本文档同主题的增强版 Skill(skills/tdd-workflow/SKILL.md)在七步流程之上补充了三个在真实工程中至关重要的环节,可作为同一工作流的上游/下游扩展:
其一,先探测测试运行器,不要想当然用 npm test。 增强版用 <test>、<test-watch>、<coverage> 作为占位符,并给出 npm/pnpm/yarn/Bun 的运行器命令矩阵(如 Bun 原生运行器用 bun test --coverage 而非 bun run test),强调"包管理器 ≠ 测试运行器"——一个项目可以既装依赖用 Bun、跑测试用 Jest。ECC 为此还提供了官方探测器 scripts/setup-package-manager.js,按 CLAUDE_PACKAGE_MANAGER → .claude/package-manager.json → package.json 的 packageManager 字段 → 锁文件 → 全局配置的顺序解析包管理器。
其二,用 Git 检查点提交固化每个阶段证据。 推荐的最简工作流是一个 RED 提交(test: add reproducer for <feature or bug>)+ 一个 GREEN 提交(fix: <feature or bug>)+ 一个可选重构提交(refactor: ...),且只把当前活动分支上、当前任务序列内可达的提交计为有效证据。
其三,输出人类可读的 TDD 证据报告。 在 GREEN 与覆盖率确认后,把用户旅程、每个任务的执行摘要与实际运行命令、RED/GREEN 输出摘录、测试规格表("什么被保证了、由哪个测试保证、类型、结果、证据命令")、覆盖率与已知缺口、合并证据一并落盘到 docs/testing/<task>.tdd.md 之类的路径,保证跨会话、跨 squash merge 之后仍可审计。
测试模式样板:单元、API、E2E 三类全量示例
Skill 为三类测试各给出了一份可运行的样板代码,这些示例同时承担"格式规范"与"断言风格规范"双重职责,应原样保留在工作流中。
单元测试模式(Jest/Vitest + Testing Library)
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'
describe('Button Component', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('calls onClick when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click</Button>)
fireEvent.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Click</Button>)
expect(screen.getByRole('button')).toBeDisabled()
})
})
注意其中的测试风格要点:通过 getByText / getByRole 这种语义化查询定位元素,断言的是"用户看到/交互的行为",而非组件内部实现细节。
API 集成测试模式(Next.js App Router)
import { NextRequest } from 'next/server'
import { GET } from './route'
describe('GET /api/markets', () => {
it('returns markets successfully', async () => {
const request = new NextRequest('http://localhost/api/markets')
const response = await GET(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(Array.isArray(data.data)).toBe(true)
})
it('validates query parameters', async () => {
const request = new NextRequest('http://localhost/api/markets?limit=invalid')
const response = await GET(request)
expect(response.status).toBe(400)
})
it('handles database errors gracefully', async () => {
// Mock database failure
const request = new NextRequest('http://localhost/api/markets')
// Test error handling
})
})
这个模式体现集成测试的黄金三角:成功路径 200、参数校验 400、外部依赖故障时的优雅降级——绝不只测 happy path。
E2E 测试模式(Playwright)
import { test, expect } from '@playwright/test'
test('user can search and filter markets', async ({ page }) => {
// Navigate to markets page
await page.goto('/')
await page.click('a[href="/markets"]')
// Verify page loaded
await expect(page.locator('h1')).toContainText('Markets')
// Search for markets
await page.fill('input[placeholder="Search markets"]', 'election')
// Wait for debounce and results
await page.waitForTimeout(600)
// Verify search results displayed
const results = page.locator('[data-testid="market-card"]')
await expect(results).toHaveCount(5, { timeout: 5000 })
// Verify results contain search term
const firstResult = results.first()
await expect(firstResult).toContainText('election', { ignoreCase: true })
// Filter by status
await page.click('button:has-text("Active")')
// Verify filtered results
await expect(results).toHaveCount(3)
})
test('user can create a new market', async ({ page }) => {
// Login first
await page.goto('/creator-dashboard')
// Fill market creation form
await page.fill('input[name="name"]', 'Test Market')
await page.fill('textarea[name="description"]', 'Test description')
await page.fill('input[name="endDate"]', '2025-12-31')
// Submit form
await page.click('button[type="submit"]')
// Verify success message
await expect(page.locator('text=Market created successfully')).toBeVisible()
// Verify redirect to market page
await expect(page).toHaveURL(/\/markets\/test-market/)
})
E2E 用例的关键质量信号包括:用 data-testid 与语义文本选择器而不是脆弱的 CSS class;对异步结果用带 timeout 的 toHaveCount/toBeVisible 做轮询断言而非裸 sleep;用 toHaveURL 正则校验表单提交后的路由跳转。
补充:Bun 原生测试模式(bun:test)
当项目实际使用 Bun 内建运行器时(判断依据:scripts.test 是 bun test,或测试文件 import { test, expect } from "bun:test"),应从 bun:test 导入并使用 bun test(含 --watch、--coverage 参数)运行,用 mock.module(...)/mock(...) 替代 jest.mock(...),覆盖率阈值配置在 bunfig.toml 的 [test] 段而不是 Jest 的 coverageThresholds 块。相关运行时细节可进一步参考同仓库的 bun-runtime skill。
测试文件组织规范
Skill 给出了按"组件就近放单元测试、API 就近放集成测试、顶层集中放 E2E"的目录约定:
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx # Unit tests
│ │ └── Button.stories.tsx # Storybook
│ └── MarketCard/
│ ├── MarketCard.tsx
│ └── MarketCard.test.tsx
├── app/
│ └── api/
│ └── markets/
│ ├── route.ts
│ └── route.test.ts # Integration tests
└── e2e/
├── markets.spec.ts # E2E tests
├── trading.spec.ts
└── auth.spec.ts
这套结构的核心逻辑是发现成本最小化:Button.test.tsx 紧挨 Button.tsx,开发者改动组件时能立刻看到对应测试;route.test.ts 与 route.ts 同目录,路由行为与路由测试天然对齐;e2e/ 独立成目录并按业务域(markets/trading/auth)拆分 .spec.ts,避免把慢速的浏览器测试混入毫秒级的单元测试。ECC 仓库自身的 tests 目录(lib/hooks/commands/scripts/ci/integration/docs 等细分)在更大规模上实践了同样的"按被测对象域分组"思路。
Mock 外部服务:隔离单元测试的三种标准姿势
集成测试验证真实协作,而单元测试必须隔离一切外部依赖。Skill 针对语义搜索类应用最常见的三个外部服务给出了标准 Mock 模板。
Supabase Mock(数据库层)
jest.mock('@/lib/supabase', () => ({
supabase: {
from: jest.fn(() => ({
select: jest.fn(() => ({
eq: jest.fn(() => Promise.resolve({
data: [{ id: 1, name: 'Test Market' }],
error: null
}))
}))
}))
}
}))
Redis Mock(缓存/向量检索层)
jest.mock('@/lib/redis', () => ({
searchMarketsByVector: jest.fn(() => Promise.resolve([
{ slug: 'test-market', similarity_score: 0.95 }
])),
checkRedisHealth: jest.fn(() => Promise.resolve({ connected: true }))
}))
注意这个 Mock 刻意暴露了 similarity_score 字段与 checkRedisHealth 健康检查函数——前者支撑"按相似度排序"用例的断言,后者支撑"Redis 不可用时的降级路径"用例。
OpenAI Mock(嵌入向量生成层)
jest.mock('@/lib/openai', () => ({
generateEmbedding: jest.fn(() => Promise.resolve(
new Array(1536).fill(0.1) // Mock 1536-dim embedding
))
}))
1536 维的模拟嵌入向量意味着测试完全不依赖真实模型调用,既保证确定性输出,又避免每次跑测试都产生 API 费用。三份模板共同传达的纪律是:凡是越过进程边界的东西(数据库、缓存、第三方模型 API)都要 mock,且 mock 的返回值要与真实接口的形状一一对应,否则会出现"测试通过、集成后立刻崩"的假阳性。
覆盖率验证与阈值配置
npm run test:coverage
Skill 给出的 Jest 覆盖率门槛配置把四个维度全部钉在 80%:
{
"jest": {
"coverageThresholds": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
四个指标含义各不相同,80% 只落在"行覆盖"上是不够的:branches(分支)盯住 if/ternary 两侧是否都被走到,functions 盯住是否有未被调用的死函数,lines 与 statements 盯住语句级执行。这在 ECC 自身配置中也能看到对应实践:package.json 的 "coverage" 脚本用 c8 同时约束 lines/functions/branches/statements(其中 branches 卡在 79),说明分支覆盖往往是四个维度中最难到 80% 的一环,值得优先补测。
常见测试误区与正确写法(Anti-Patterns)
Skill 用"FAIL: WRONG / PASS: CORRECT"双栏对照的方式列出了三类高频错误,这是自查测试质量时最直接的检查清单。
误区一:测试实现细节
// FAIL: WRONG: Testing Implementation Details
// Don't test internal state
expect(component.state.count).toBe(5)
// PASS: CORRECT: Test User-Visible Behavior
// Test what users see
expect(screen.getByText('Count: 5')).toBeInTheDocument()
判据:一旦重构内部实现就变红的测试,往往绑定了实现细节;而断言"用户屏幕上出现什么"的测试才是真行为测试。
误区二:脆弱的 CSS 选择器
// FAIL: WRONG: Brittle Selectors
// Breaks easily
await page.click('.css-class-xyz')
// PASS: CORRECT: Semantic Selectors
// Resilient to changes
await page.click('button:has-text("Submit")')
await page.click('[data-testid="submit-button"]')
判据:CSS class 是样式实现细节,改一下 tailwind 类名测试就全线崩溃;语义选择器与 data-testid 面向交互契约,对样式变更免疫。
误区三:测试间互相依赖
// FAIL: WRONG: No Test Isolation
// Tests depend on each other
test('creates user', () => { /* ... */ })
test('updates same user', () => { /* depends on previous test */ })
// PASS: CORRECT: Independent Tests
// Each test sets up its own data
test('creates user', () => {
const user = createTestUser()
// Test logic
})
test('updates user', () => {
const user = createTestUser()
// Update logic
})
判据:每个测试必须自行准备数据(Arrange),串行依赖共享状态的测试一旦调整执行顺序就会产生随机失败。ECC 的 tdd-guide Agent 还补充了第四条反模式:断言太少——"通过却什么都没验证"的测试比失败更危险。
持续测试:Watch、Pre-Commit 与 CI
TDD 不是一次性动作,Skill 把它嵌入开发节奏的三个触点。
开发中的 Watch 模式
npm test -- --watch
# Tests run automatically on file changes
写实现时让测试文件变更自动触发重跑,把 RED→GREEN 的反馈周期压缩到秒级。
Pre-Commit 钩子
# Runs before every commit
npm test && npm run lint
提交前强制门禁。在 ECC 生态中这不仅是建议:仓库的 tdd-reminder.kiro.hook 会在新建 .ts/.tsx 文件时自动向 Agent 提问"这个文件是否需要对应测试覆盖,如有逻辑建议按 TDD 原则补建测试文件",而 quality-gate.kiro.hook 则把"build + typecheck + lint + tests"一键串联执行(底层脚本见 .kiro/scripts/quality-gate.sh,它会自动探测 pnpm/yarn/bun/npm 并按项目配置选择 Biome/ESLint/Ruff 等工具,任何一步失败即整体 exit 1)。
CI/CD 集成
# GitHub Actions
- name: Run Tests
run: npm test -- --coverage
- name: Upload Coverage
uses: codecov/codecov-action@v3
跑测试并上传覆盖率报告,让覆盖率阈值在每次合并请求上强制生效,防止"本地过了、CI 没跑"的回退。
十条最佳实践与成功度量
Skill 以十条最佳实践收束全部流程,它们同时是每个测试文件的隐性评审标准:
- Write Tests First —— 永远 TDD
- One Assert Per Test —— 每个测试聚焦单一行为
- Descriptive Test Names —— 测试名说清楚"测了什么"
- Arrange-Act-Assert —— 清晰的三段式结构
- Mock External Dependencies —— 隔离单元测试
- Test Edge Cases —— null、undefined、空值、大输入
- Test Error Paths —— 不只测 happy path
- Keep Tests Fast —— 每个单元测试 < 50ms
- Clean Up After Tests —— 不留副作用
- Review Coverage Reports —— 用报告找覆盖缺口
ECC 的 tdd-guide Agent 在此基础上把必须覆盖的边界情况展开为八类:Null/Undefined 输入、空数组/字符串、非法类型、极值(min/max)、错误路径(网络与数据库故障)、竞态条件(并发操作)、大数据(10k+ 项的性能)、特殊字符(Unicode、emoji、SQL 字符),可作为"边界覆盖"的兜底核对清单。
当以上流程全部执行完毕后,用 Skill 定义的成功度量检验交付质量:
- 达成 80%+ 代码覆盖率
- 全部测试通过(green)
- 无跳过(skipped)或禁用(disabled)的测试
- 测试执行快速(单元测试 < 30s)
- E2E 覆盖关键用户流程
- 测试在上生产前就拦下了 Bug
ECC 中 TDD 的完整落地回路
把仓库内各组件串联起来,可以看到 Skill 之上还有一层"监督与提醒"能力:开发会话开始时,.kiro/steering/testing.md 以 auto inclusion 姿态把测试要求注入上下文;写新功能时 Skill 定义七步流程、tdd-guide Agent(其机器可读配置见 .kiro/agents/tdd-guide.json,声明了 read/write/shell 三类允许工具并内置 v1.8 Eval-Driven TDD 增补:先定义 capability 与回归 eval、跑基线记录失败特征、做最小通过改动、复跑并报告 pass@1/pass@3,发布关键路径在合并前要求 pass^3 稳定性)实时把关;新建 TS 文件时 tdd-reminder Hook 自动弹出提醒;提交或触发质量门禁时,quality-gate.sh 用真实命令完成最后裁决。这一回路保证了 Skill 文档中的每条规则都有"能感知、能提醒、能拦截"的执行载体。
最后回到 Skill 作者留下的那句话:测试不是可选项——它是让你敢于重构、快速迭代、并对线上可靠性有底气的安全网。在 ECC 的 agent harness 场景下,这条安全网同样保护着不断演进的 Skill、Rules 与集成脚本本身,正如仓库 CI 会把每次改动重新跑进 80%+ 覆盖率门槛那样。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00