测试覆盖率是大家都认为重要但没人愿意负责提升的指标之一。您知道这个模式:代码库从 90% 覆盖率开始,然后几个功能没写测试就上线了,然后一次重构跳过了测试更新因为”我们会回来补”。六个月后您降到了 60%,攀回去感觉像一份没人报名的全职工作。
这个案例研究展示了一个团队如何使用带编程 Agent 的 JieGou 工作流在三周内从 60% 提升到 94% 的测试覆盖率——自动完成,最少人工干预。
问题
相关的代码库是一个包含 312 个模块的 Node.js 后端。覆盖率已经稳步下降了数月:
- 60% 的总体测试覆盖率,年初时为 85%
- 47 个模块零测试——主要是工具函数、数据转换器和验证逻辑
- 新功能没有测试就上线,因为开发者专注于交付截止日期
- CI 管道运行测试但不强制覆盖率阈值,所以下降在有人检查之前是不可见的
团队曾两次尝试分配”测试编写冲刺”。两次都因客户升级问题而被降低优先级。手动编写测试缓慢、枯燥,且与每个其他优先级竞争。
工作流
团队在 JieGou 中构建了一个 4 步工作流,在代码合并到 main 时自动触发。除非生成的测试需要修改,否则不需要手动干预。
步骤 1:触发 —— PR 合并时的 GitHub webhook
工作流从 pull request 合并到 main 时触发的 webhook 开始。
trigger:
type: webhook
source: github
events: ["pull_request.closed"]
filters:
- field: "action"
value: "closed"
- field: "pull_request.merged"
value: true
- field: "pull_request.base.ref"
value: "main"
webhook 载荷包含更改文件列表、合并提交 SHA 和 PR 元数据。所有这些都输入到下一步。
步骤 2:分析 —— 识别未测试的函数
一个配方步骤获取 webhook 中的更改文件列表,并与最新的覆盖率报告进行交叉引用。配方提示很直接:
step: analyze-coverage
type: recipe
model: claude-sonnet-4-6
input:
changed_files: "{{trigger.pull_request.changed_files}}"
coverage_report: "{{secrets.COVERAGE_REPORT_URL}}"
prompt: |
You are a test coverage analyst. Given the list of changed files
and the coverage report (lcov format), identify:
1. Which changed files have less than 80% line coverage
2. Which exported functions in those files have zero coverage
3. The function signatures and a brief description of what each does
Output a JSON array of objects with fields:
file_path, function_name, signature, description, current_coverage
这一步产生一个精确的列表,指出哪些函数需要测试。它过滤掉已经有足够覆盖率的文件,这样编程 agent 就不会在已经测试过的代码上浪费时间。
步骤 3:生成 —— 编程 Agent 编写测试
这是编程 Agent 发挥主要作用的地方。它接收未测试函数列表,克隆仓库,并为每个函数编写单元测试。
step: generate-tests
type: coding-agent
model: claude-sonnet-4-6
repo: "{{secrets.REPO_URL}}"
branch: "test-gen/{{trigger.pull_request.number}}"
maxTurns: 30
sandbox:
memory: "1GB"
timeout: "5m"
network: false
tools:
- read
- write
- edit
- bash
- glob
- grep
input:
untested_functions: "{{steps.analyze-coverage.output}}"
task: |
You are a senior test engineer. Your job is to write unit tests for
the following untested functions:
{{untested_functions}}
Instructions:
1. Read the source file for each function to understand its behavior
2. Read existing test files in the same directory for style conventions
3. Write tests using the project's test framework (vitest)
4. Each test file should cover: happy path, edge cases, error handling
5. Run `npm test -- --reporter=verbose <test-file>` after writing each
test file to verify all tests pass
6. If a test fails, read the error, fix the test, and re-run
7. Do NOT modify source code — only create or update test files
Match the existing test style: describe blocks, clear test names,
arrange-act-assert pattern. Use the existing fixtures and helpers
where available.
关键配置选择:
- 模型:Claude Sonnet 4.6 —— 足够快以进行大量测试生成,足够智能以理解复杂的函数签名和边缘情况。
- 沙盒:1 GB 内存、5 分钟超时 —— 为
npm test运行提供足够空间而不会有失控进程的风险。 - 网络禁用 —— agent 不需要外部访问;一切都在克隆的仓库中。
- maxTurns: 30 —— 允许 agent 在多个测试文件上迭代并在多个周期中修复失败。
任务描述包含一个关键指令:先阅读现有测试文件。这确保生成的测试与团队的约定一致——相同的断言风格、相同的 describe/it 结构、相同的 fixture 模式。没有这个,生成的测试技术上正确但风格不一致,在审查时会造成摩擦。
步骤 4:提交 —— 创建包含结果的 PR
编程 agent 完成后,最后一个配方步骤创建包含生成测试的 pull request,并在 PR 描述中包含测试结果。
step: submit-pr
type: recipe
model: claude-sonnet-4-6
input:
modified_files: "{{steps.generate-tests.modified_files}}"
test_output: "{{steps.generate-tests.output}}"
source_pr: "{{trigger.pull_request.number}}"
prompt: |
Create a pull request with the following:
- Title: "test: auto-generated tests for PR #{{source_pr}}"
- Body: Include a summary of tests added, functions covered,
and the full test output showing all tests passing
- Label: "auto-tests"
- Request review from the original PR author
PR 被标记为 auto-tests,这样团队可以过滤和批量审查生成的测试 PR。请求原始 PR 作者审查意味着最熟悉代码的人首先看到测试。
结果
团队在每次合并到 main 时运行此工作流三周。以下是数据:
| 指标 | 值 |
|---|---|
| 起始覆盖率 | 60% |
| 最终覆盖率 | 94% |
| 生成的测试 | 847 |
| 创建的 Pull Request | 42 |
| 平均 PR 审查时间 | 12 分钟 |
| 发现现有 bug 的测试 | 23 |
| 平均每日成本 | 约 $4.50(Claude API) |
有几点值得注意。
42 个 PR 中的 847 个测试意味着每个 PR 大约 20 个测试。大多数 PR 覆盖 2-4 个源文件,每个函数 5-6 个测试(快乐路径、边界条件、错误情况、null/undefined 输入、类型转换边缘情况)。
12 分钟的平均审查时间很出色。大多数生成的测试在第一次通过就是正确的。主要审查意见是风格性的——重命名测试描述、调整 fixture 数据使其更真实。很少有测试需要功能性修正。
23 个测试发现了实际 bug。这是最有趣的结果。编程 agent 为一个日期解析函数编写了测试,该函数期望 ISO 8601 格式,测试揭示该函数对带时区偏移的时间戳默默返回 Invalid Date。那个 bug 已经在生产环境中存在了四个月。在验证逻辑、分页助手中的偏差一错误和缓存模块中的竞态条件中也有类似发现。
每天 $4.50 的 Claude API 成本与节省的工程时间相比微不足道。保守估计:以每个测试 15 分钟手动编写 847 个测试大约需要 212 小时——一个全职工程师五周以上的时间。
经验教训
运行此工作流三周后,团队根据有效和无效的方面完善了方法。
从纯函数开始
第一个版本的工作流针对所有未测试的代码。这对纯函数效果很好——工具函数、验证器、转换器、格式化器——因为它们有清晰的输入、清晰的输出且没有副作用。Agent 可以阅读函数、理解契约并编写全面的测试。
具有数据库连接、外部 API 调用和共享可变状态的复杂模块更难。Agent 有时会生成 mock 过多或测试实现细节而非行为的测试。团队调整了分析步骤,优先处理纯函数,将复杂模块排入手动审查队列。
提供现有测试示例
最具影响力的单一更改是在任务描述中添加此指令:“阅读同一目录中的现有测试文件以了解风格约定。“在此指令之前,agent 产生的测试功能上正确但使用不同的断言模式、不同的 describe 块结构和不同的命名约定。在此指令之后,生成的测试与手写的几乎无法区分。
在创建 PR 之前运行测试
工作流的早期迭代提交了未经验证的测试 PR。有些有导入错误、缺少的 fixture 或断言失败。在编程 agent 任务中添加 npm test 步骤——并指示它在完成前修复失败——几乎消除了所有这些问题。Agent 的迭代循环(编写测试、运行测试、阅读错误、修复测试、重新运行)正是人工会做的方式,但更快。
审查质量,而不仅仅是覆盖率
覆盖率是代理指标。一个断言 expect(result).toBeDefined() 的测试技术上覆盖了函数但没有验证行为。团队为生成的测试 PR 添加了审查检查清单:
- 每个测试是否验证有意义的行为,而不仅仅是函数运行?
- 是否覆盖了边缘情况(null 输入、空数组、边界值)?
- 测试描述是否清楚地说明正在测试什么行为?
- 断言是否具体(
.toBe(42)而非.toBeTruthy())?
大多数生成的测试无需更改即通过此检查清单。少数不通过的在 12 分钟的审查中很容易发现和修复。
完整配置
对于想要复制此工作流的团队,这里是完整的步骤配置:
workflow:
name: "Auto Test Generation"
description: "Generate unit tests for untested code on PR merge"
trigger:
type: webhook
source: github
events: ["pull_request.closed"]
filters:
- field: "pull_request.merged"
value: true
- field: "pull_request.base.ref"
value: "main"
steps:
- id: analyze-coverage
type: recipe
model: claude-sonnet-4-6
# ...(见上方步骤 2)
- id: generate-tests
type: coding-agent
model: claude-sonnet-4-6
dependsOn: [analyze-coverage]
# ...(见上方步骤 3)
- id: submit-pr
type: recipe
model: claude-sonnet-4-6
dependsOn: [generate-tests]
# ...(见上方步骤 4)
工作流端到端运行 8-15 分钟,取决于未测试函数的数量。大部分时间是编程 agent 遍历测试文件并在每个文件后运行 npm test。
接下来
团队现在正在两个方向扩展工作流:
- 集成测试 —— 一个单独的编程 agent 步骤,使用现有的测试助手和数据库 fixture 为 API 端点生成集成测试
- 覆盖率强制 —— 一个 CI 检查,如果覆盖率降到 90% 以下则使构建失败,现在基线已经高到可以强制执行
更广泛的教训:测试覆盖率不必是手动苦差事。有了正确的工作流,您可以将其视为与现有 CI/CD 并行运行的自动化管道——无需专门的冲刺、无需降低优先级、无需覆盖率债务。
编程 Agent 在 Pro 计划及以上可用。构建您的第一个编程工作流。