首页
/ ECC TDD Workflow:测试先行、80% 覆盖率门禁与测试运行器自动检测的完整 TDD 实战指南

ECC TDD Workflow:测试先行、80% 覆盖率门禁与测试运行器自动检测的完整 TDD 实战指南

2026-09-04 15:14:28作者:贡沫苏Truman

本篇指南围绕 ECC 仓库中 tdd-workflow 技能 展开,系统讲解 ECC 如何将测试驱动开发(TDD)固化为一条可执行、可门禁的开发流程:从开发前检测项目的真实测试运行器(npm / pnpm / yarn / Bun),到 RED-GREEN-REFACTOR 循环、单元/集成/E2E 三层测试模式、覆盖率阈值配置与常见测试反模式。读完本文,你可以在新特性开发、缺陷修复与重构场景中直接套用这套 TDD 工作流,并结合 ECC 自带的包管理器检测脚本 scripts/setup-package-manager.js 正确解析 <test> / <coverage> 占位命令,避免"用错运行器导致测试无法启动"这一高频错误。

技能定位与激活时机

ECC 中的 tdd-workflow 是一个面向 AI Agent 的开发流程技能(skill),其元数据声明了明确的用途边界:

---
name: tdd-workflow
description: Use this skill when writing new features, fixing bugs, or refactoring code.
             Enforces test-driven development with 80%+ coverage including unit, integration, and E2E tests.
---

也就是说,它不是泛泛的"测试建议",而是一个强制性的流程约束:所有开发必须测试先行,且最终覆盖率(单元 + 集成 + E2E 合计)不低于 80%。技能附带的 agents/openai.yaml 配置了 allow_implicit_invocation: true,意味着 Agent 在相关任务中会隐式启用该流程,其默认提示为"Use $tdd-workflow to drive the change with tests before implementation."

按技能定义,以下场景必须激活 TDD 工作流:

  • 编写新特性或新功能
  • 修复缺陷(bug / issue)
  • 重构既有代码
  • 新增 API 端点
  • 创建新组件

三条核心原则贯穿整个流程:

  1. 测试先于代码——永远先写测试,再写让测试通过的实现;
  2. 覆盖率要求——最低 80% 覆盖(单元 + 集成 + E2E),所有边缘用例、错误场景、边界条件必须有对应测试;
  3. 三层测试类型——单元测试(函数/工具/纯函数/组件逻辑)、集成测试(API 端点/数据库操作/服务交互/外部 API 调用)、E2E 测试(Playwright,覆盖关键用户流、完整工作流、浏览器自动化与 UI 交互)。

Step 0:检测测试运行器(不要假设 npm test)

这是 ECC TDD 工作流中最具工程经验的一步:先解析项目真实使用的测试运行器,再开始 RED 阶段。技能文档明确要求"不要假设 npm test",并定义了 <test><test-watch><coverage> 三个占位符,要求在流程开始之前一次性解析完毕,后续所有步骤中的 npm test 都要替换为解析结果。

运行 ECC 自带的包管理器检测器

技能给出的第一步命令是:

node scripts/setup-package-manager.js --detect

该脚本位于 ECC 仓库的 scripts/setup-package-manager.js,支持 --detect(检测并展示当前包管理器)、--global <pm>(写入 ~/.claude/package-manager.json)、--project <pm>(写入 .claude/package-manager.json)、--list(列出可用包管理器)等选项。

检测的核心实现在 scripts/lib/package-manager.jsgetPackageManager() 函数中,从源码可以看到完整的六级优先级链:

  1. 环境变量 CLAUDE_PACKAGE_MANAGERsource: 'environment');
  2. 项目级配置 .claude/package-manager.jsonsource: 'project-config');
  3. package.jsonpackageManager 字段(source: 'package.json',支持 pnpm@8.6.0 这类带版本的格式,取 @ 前缀部分匹配);
  4. 锁文件检测(source: 'lock-file');
  5. 全局用户配置 ~/.claude/package-manager.jsonsource: 'global-config');
  6. 兜底默认 npm(source: 'default')。

源码中有两个值得注意的实现细节:

  • 锁文件别名:Bun 的锁文件从二进制的 bun.lockb 切换为文本格式的 bun.lock,代码中通过 lockFileAliases: ['bun.lockb'] 同时兼容新旧格式。各管理器的锁文件映射为:npm → package-lock.json、pnpm → pnpm-lock.yaml、yarn → yarn.lock、bun → bun.lock(含 bun.lockb 别名)。
  • 性能与稳定性防护getAvailablePackageManagers() 会 spawn 子进程(Windows 上的 where.exe / Unix 上的 which)探测已安装的包管理器,源码注释明确警告不要在会话启动钩子中调用它——历史上这曾在 Windows 上因超出 Bun 的 spawn 限制而冻结插件(注释中引用了 issue #162)。因此默认兜底路径直接返回 npm,不再探测系统。

另外,getRunCommand() 对脚本名做了安全校验(SAFE_NAME_REGEX = /^[@a-zA-Z0-9_./-]+$/),防止 shell 元字符注入,同时允许 @scope/pkg 这类作用域包名。

包管理器 ≠ 测试运行器

技能特别强调:用 Bun 安装依赖的项目,测试仍可能跑在 Jest 或 Vitest 上。区分方法是检查 package.jsonscripts.test 与测试文件本身:

  • scripts.test 调用 jest / vitest → 通过检测到的包管理器运行(npm testpnpm testyarn testbun run test);
  • scripts.test 就是 bun test,或测试文件 import { test, expect } from "bun:test",或无 jest/vitest 配置但存在 Bun → 使用 Bun 原生运行器bun test)。

据此得到完整的运行器命令矩阵:

Runner <test> <test-watch> <coverage> <lint>
npm npm test npm test -- --watch npm run test:coverage npm run lint
pnpm pnpm test pnpm test --watch pnpm test:coverage pnpm lint
yarn yarn test yarn test --watch yarn test:coverage yarn lint
Bun(脚本跑 jest/vitest) bun run test bun run test --watch bun run test:coverage bun run lint
Bun(原生 bun:test bun test bun test --watch bun test --coverage bun run lint

技能文档指出一个高频陷阱:bun test(Bun 内置运行器)和 bun run test(执行 package.jsontest 脚本)不是同一件事。在 ESM-only 项目中用 npx / bun run 去调 Jest 可能直接跑不起来,而 bun test 则以原生方式执行整个测试套件。因此在 RED 门禁之前必须先确认项目期望哪种,并把 <test> / <coverage> 替换到位。ECC 的 bun-runtime 技能 也印证了这一点:Bun 的 bun test 提供 Jest-like API,bun install 默认生成文本格式 bun.lock(旧版本为二进制 bun.lockb),可作为统一工具链覆盖 run + install + test + build。

以 ECC 仓库自身为例,其 package.json 使用 npm(同时存在 package-lock.jsonyarn.lock),测试脚本通过 tests/run-all.js 组织运行——这正是一个"锁文件检测 + scripts.test 检查"流程的真实用例。

TDD 七步循环:从用户旅程到覆盖率验证

运行器解析完成后,进入标准 TDD 循环。技能将其定义为 Step 1 至 Step 7。

Step 1:编写用户旅程

以验收标准的形式描述行为:

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:生成测试用例

为每个用户旅程生成覆盖正常路径、边缘情况、降级行为与排序逻辑的测试骨架:

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:运行测试(此时应失败)

<test>
# Tests should fail - we haven't implemented yet

这一步就是 RED 门禁:测试必须真实失败,才能证明测试确实覆盖了待实现的行为。

Step 4:实现最小代码

只写让测试通过的最小实现:

// Implementation guided by tests
export async function searchMarkets(query: string) {
  // Implementation here
}

Step 5:再次运行测试(此时应通过)

<test>
# Tests should now pass

Step 6:重构

在测试保持全绿的前提下提升代码质量:去重、改进命名、优化性能、增强可读性。

Step 7:验证覆盖率

<coverage>
# Verify 80%+ coverage achieved

三类测试的代码模式

单元测试模式(Jest/Vitest + Testing Library)

以 React 组件为例,测试面向用户可见行为而非内部实现:

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()
  })
})

Bun 原生测试模式(bun:test

当 Step 0 判定项目使用 Bun 内置运行器时,从 bun:test 导入并以 bun test(而非 bun run test)执行。API 是 Jest-like 的,describe / it / expect 与多数 matcher 可以直接迁移:

import { describe, it, expect, mock } from 'bun:test'
import { searchMarkets } from './search'

describe('searchMarkets', () => {
  it('returns an empty list for an empty query', async () => {
    expect(await searchMarkets('')).toEqual([])
  })

  it('sorts results by similarity score', async () => {
    const results = await searchMarkets('election')
    expect(results).toEqual([...results].sort((a, b) => b.score - a.score))
  })
})
bun test              # run once (RED/GREEN gate)
bun test --watch      # watch mode during development
bun test --coverage   # coverage report

两个 Bun 特有的差异点需要注意:模块 mock 使用 bun:testmock.module(...) / mock(...),替代 jest.mock(...);覆盖率阈值配置放在 bunfig.toml[test] 段(如 coverageThreshold),而不是 Jest 的 coverageThresholds 配置块。

API 集成测试模式

直接以 NextRequest 构造请求调用 route handler,验证状态码、响应结构与参数校验:

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
  })
})

E2E 测试模式(Playwright)

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/)
})

ECC 仓库另有 e2e-testing 技能 补充了 Page Object Model、playwright.config.ts 组织方式与 CI 中 flaky 测试治理等更细粒度的 Playwright 模式,可作为 E2E 层的延伸阅读。

测试文件组织

技能给出的目录约定是"测试与实现同目录 + 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

约定要点:单元/集成测试以 *.test.ts(x) 与被测文件同目录放置,E2E 测试统一收敛到 e2e/ 目录并以 *.spec.ts 命名,从而在运行器配置层面就能区分三层测试的执行时机与资源开销。

Mock 外部服务

三个典型的 jest.mock 工厂示例,分别隔离数据库、向量检索与模型调用:

Supabase(数据库):

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(向量检索):

jest.mock('@/lib/redis', () => ({
  searchMarketsByVector: jest.fn(() => Promise.resolve([
    { slug: 'test-market', similarity_score: 0.95 }
  ])),
  checkRedisHealth: jest.fn(() => Promise.resolve({ connected: true }))
}))

OpenAI(嵌入生成,注意 mock 出正确维度的向量):

jest.mock('@/lib/openai', () => ({
  generateEmbedding: jest.fn(() => Promise.resolve(
    new Array(1536).fill(0.1) // Mock 1536-dim embedding
  ))
}))

Mock 的意义在于把单元/集成测试与真实外部依赖解耦:测试验证的是"当依赖返回 X 时,被测代码的行为",而不是依赖本身是否可用。Bun 原生运行器下则改用 mock.module(...) / mock(...)

覆盖率验证与阈值门禁

运行 <coverage> 生成报告后,用框架级阈值把"80%"从口号变成硬门禁。技能给出的 Jest 配置示例:

{
  "jest": {
    "coverageThresholds": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

四个维度(branches / functions / lines / statements)全部设 80,意味着任何一维低于阈值都会使覆盖率检查失败。Bun 项目则把等效的阈值写在 bunfig.toml[test] 段。

ECC 仓库中的 test-coverage 命令 进一步把"分析覆盖率报告 → 定位低于 80% 的文件 → 按 happy path / 错误处理 / 边缘用例 / 分支覆盖的优先级补齐测试"固化为可复用流程,并且它的"检测测试框架"表覆盖了 Jest、Vitest、pytest、cargo llvm-cov、JaCoCo、go test -coverprofile 等多种语言与框架——与 tdd-workflow 的 JS 视角互为补充。

常见测试反模式(FAIL vs PASS 对照)

技能用三组 FAIL/PASS 对照总结了最容易犯的错误:

反模式一:测实现细节 vs 测用户可见行为

// FAIL: WRONG: Testing Implementation Details
expect(component.state.count).toBe(5)
// PASS: CORRECT: Test User-Visible Behavior
expect(screen.getByText('Count: 5')).toBeInTheDocument()

反模式二:脆弱选择器 vs 语义化选择器

// FAIL: WRONG: Brittle Selectors
await page.click('.css-class-xyz')
// PASS: CORRECT: Semantic Selectors
await page.click('button:has-text("Submit")')
await page.click('[data-testid="submit-button"]')

反模式三:测试间相互依赖 vs 测试独立

// FAIL: WRONG: No Test Isolation
test('creates user', () => { /* ... */ })
test('updates same user', () => { /* depends on previous test */ })
// PASS: CORRECT: Independent Tests
test('creates user', () => {
  const user = createTestUser()
  // Test logic
})

test('updates user', () => {
  const user = createTestUser()
  // Update logic
})

持续测试:Watch、Pre-Commit 与 CI

技能定义了三个持续测试触点:

开发期 Watch 模式——文件变更自动重跑:

<test-watch>
# Tests run automatically on file changes

Pre-Commit Hook——每次提交前执行测试 + lint:

# Runs before every commit
<test> && <lint>

CI/CD 集成——以 GitHub Actions 为例上传覆盖率:

- name: Run Tests
  run: <coverage>
- name: Upload Coverage
  uses: codecov/codecov-action@v3

三处统一使用 Step 0 解析出的占位命令,保证本地与 CI 行为一致。

最佳实践与成功指标

技能的十条最佳实践:

  1. 测试先行——始终 TDD;
  2. 一个测试一个断言主题——聚焦单一行为;
  3. 描述性命名——测试名说明测的是什么;
  4. Arrange-Act-Assert——清晰的测试结构;
  5. Mock 外部依赖——隔离单元测试;
  6. 测边缘用例——null、undefined、空、超大输入;
  7. 测错误路径——不只有 happy path;
  8. 保持测试快速——单元测试单条 < 50ms;
  9. 测试后清理——无副作用残留;
  10. 审查覆盖率报告——主动找缺口。

对应的成功指标(完成门禁):

  • 达成 80%+ 代码覆盖率;
  • 全部测试通过(green);
  • 无跳过或禁用的测试;
  • 测试执行快(单元测试 < 30s);
  • E2E 覆盖关键用户流;
  • 测试能在进入生产之前拦截缺陷。

小结

ECC 的 tdd-workflow 技能把 TDD 从"方法论"落成了"可执行流程":Step 0 用 scripts/setup-package-manager.jsscripts/lib/package-manager.js 的六级检测链确定包管理器,再用 scripts.test 与测试文件判定真实运行器,从而让 RED/GREEN 门禁建立在正确的命令之上;随后七步循环(用户旅程 → 测试用例 → 失败确认 → 最小实现 → 通过确认 → 重构 → 覆盖率验证)配合三类测试模式、阈值门禁与三组反模式对照,构成一套可以在任意 JS/TS 项目中直接照搬的 TDD 操作手册。其核心理念正如技能结尾所强调的:测试不是可选项,而是支撑自信重构、快速开发与生产可靠性的安全网。

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

项目优选

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