首页
/ ECC 的 TDD 工作流 Skill 实战指南:从 RED-GREEN-REFACTOR 到 80%+ 覆盖率的质量闭环

ECC 的 TDD 工作流 Skill 实战指南:从 RED-GREEN-REFACTOR 到 80%+ 覆盖率的质量闭环

2026-09-07 18:51:39作者:齐添朝

TDD(测试驱动开发)的难点从来不是"先写测试"这句话本身,而是如何把它变成一条可执行、可验证、可追溯的开发流水线。本文以 ECC(agent harness performance optimization system)仓库中的 tdd-workflow Skill 为核心,系统讲解它在 ECC 中的激活时机、核心原则、完整工作流步骤、单元/集成/E2E 测试模式、Mock 策略与覆盖率门槛,并结合仓库中配套的 tdd-guide Agenttdd-reminder Hookquality-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 一节中确立了三条不可妥协的原则,也是整个流程的"宪法":

  1. 测试先于代码(Tests BEFORE Code):永远先写测试,再写让测试通过的实现。
  2. 覆盖率要求(Coverage Requirements):最低 80% 覆盖率(单元 + 集成 + E2E 合计评估);所有边界情况覆盖;错误场景必须被测试;边界条件需被验证。
  3. 测试类型(Test Types)三足鼎立,缺一不可:
  • 单元测试(Unit Tests)——针对单个函数与工具方法、组件逻辑、纯函数、helpers 工具。目标是隔离验证每一块"最小逻辑单元"。
  • 集成测试(Integration Tests)——覆盖 API 端点、数据库操作、服务间交互、外部 API 调用。验证模块拼接后的真实协作。
  • E2E 测试(Playwright)——覆盖关键用户流程、完整工作流、浏览器自动化、UI 交互。从真实用户视角验证系统整体行为。

ECC 官方仓库自己的测试工程正好印证了这套分层:根目录 tests 下按 tests/libtests/hookstests/commandstests/scriptstests/citests/integrationtests/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)使用 c8scripts/**/*.jsscripts/**/*.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.jsonpackage.jsonpackageManager 字段 → 锁文件 → 全局配置的顺序解析包管理器。

其二,用 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;对异步结果用带 timeouttoHaveCount/toBeVisible 做轮询断言而非裸 sleep;用 toHaveURL 正则校验表单提交后的路由跳转。

补充:Bun 原生测试模式(bun:test

当项目实际使用 Bun 内建运行器时(判断依据:scripts.testbun 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.tsroute.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 盯住是否有未被调用的死函数,linesstatements 盯住语句级执行。这在 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 以十条最佳实践收束全部流程,它们同时是每个测试文件的隐性评审标准:

  1. Write Tests First —— 永远 TDD
  2. One Assert Per Test —— 每个测试聚焦单一行为
  3. Descriptive Test Names —— 测试名说清楚"测了什么"
  4. Arrange-Act-Assert —— 清晰的三段式结构
  5. Mock External Dependencies —— 隔离单元测试
  6. Test Edge Cases —— null、undefined、空值、大输入
  7. Test Error Paths —— 不只测 happy path
  8. Keep Tests Fast —— 每个单元测试 < 50ms
  9. Clean Up After Tests —— 不留副作用
  10. 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%+ 覆盖率门槛那样。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389