AI + 自动化测试:让 Agent 帮你写测试
·
我在上家公司写过单元测试覆盖率要求——“新增代码覆盖率不低于 80%”。
效果怎么样?大家疯狂写 mock,覆盖率到了但测的都是 1+1=2。真正容易出 bug 的边界条件、异常路径,没人测。
不是不想测——是写好测试比写业务代码还难。你要理解边界条件、构造测试数据、mock 依赖、处理异步逻辑。一个 50 行的函数,测试可能要 150 行。
AI 做这件事天然有优势——它不嫌烦、不会漏、擅长枚举边界。
整体流程
AI 读源码
→ 理解函数签名、逻辑分支、依赖
→ 生成测试用例(正常 + 边界 + 异常)
→ 执行测试(go test)
→ 分析失败原因
→ 修复代码或测试
→ 重新执行,直到全部通过
这是一个闭环——生成 → 执行 → 反馈 → 修复。
第一步:读源码,生成测试
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// 分析源码,提取函数签名和逻辑
func analyzeSource(sourceCode, fileName string) (*SourceAnalysis, error) {
prompt := fmt.Sprintf(`分析以下 Go 源码。请列出:
1. 所有导出函数(函数名、参数、返回值)
2. 每个函数的核心逻辑分支(if/switch/for/错误处理)
3. 需要 mock 的依赖(数据库调用、HTTP 请求、文件操作)
4. 可能的边界条件和异常场景
只基于代码分析,不要推测代码中没有的内容。
源码文件 %s:
%s`, fileName, sourceCode)
result, _ := callLLM("你是 Go 代码分析专家。", prompt)
return &SourceAnalysis{
Functions: parseFunctions(result),
Branches: parseBranches(result),
Dependencies: parseDependencies(result),
EdgeCases: parseEdgeCases(result),
}, nil
}
// 根据分析结果生成测试代码
func generateTests(sourceCode, fileName string, analysis *SourceAnalysis) (string, error) {
prompt := fmt.Sprintf(`为以下 Go 源码生成完整的单元测试代码。
测试要求:
1. 每个函数至少包含:正常输入测试、边界值测试、错误路径测试
2. 外部依赖必须使用 Go 原生方式 mock(接口 + mock 结构体,不使用第三方库)
3. 测试函数名遵循 Test<FuncName>_<Scenario> 格式
4. 每个 assertion 失败时给出清晰的错误信息
5. 使用 t.Run 组织子测试
源码 %s:
%s
分析结果:
- 需要测试的函数:%s
- 需要覆盖的边界条件:%s
- 需要 mock 的依赖:%s
只输出 Go 测试代码,包含完整的 package 声明和 import。`, fileName, sourceCode,
strings.Join(analysis.Functions, ", "),
strings.Join(analysis.EdgeCases, ", "),
strings.Join(analysis.Dependencies, ", "),
)
return callLLM("你是 Go 单元测试专家。", prompt)
}
示例:给一个真实函数生成测试
输入函数:
package order
import "errors"
type OrderRepo interface {
GetOrder(id int) (*Order, error)
SaveOrder(o *Order) error
}
type Order struct {
ID int
UserID int
Amount float64
Status string
}
func ProcessRefund(repo OrderRepo, orderID int) error {
order, err := repo.GetOrder(orderID)
if err != nil {
return fmt.Errorf("获取订单失败: %w", err)
}
if order == nil {
return errors.New("订单不存在")
}
if order.Status != "paid" {
return fmt.Errorf("订单状态为 %s,不可退款", order.Status)
}
if order.Amount <= 0 {
return errors.New("退款金额无效")
}
order.Status = "refunded"
return repo.SaveOrder(order)
}
AI 生成的测试代码:
package order
import (
"errors"
"testing"
)
type mockOrderRepo struct {
getOrderFunc func(int) (*Order, error)
saveOrderFunc func(*Order) error
}
func (m *mockOrderRepo) GetOrder(id int) (*Order, error) {
return m.getOrderFunc(id)
}
func (m *mockOrderRepo) SaveOrder(o *Order) error {
return m.saveOrderFunc(o)
}
func TestProcessRefund_Success(t *testing.T) {
saved := false
repo := &mockOrderRepo{
getOrderFunc: func(id int) (*Order, error) {
return &Order{ID: 1, UserID: 10, Amount: 99.0, Status: "paid"}, nil
},
saveOrderFunc: func(o *Order) error {
saved = true
if o.Status != "refunded" {
t.Errorf("期望状态 refunded, 实际 %s", o.Status)
}
return nil
},
}
err := ProcessRefund(repo, 1)
if err != nil {
t.Fatalf("不期望错误, 但返回了: %v", err)
}
if !saved {
t.Error("期望调用 SaveOrder, 但未调用")
}
}
func TestProcessRefund_OrderNotFound(t *testing.T) {
repo := &mockOrderRepo{
getOrderFunc: func(id int) (*Order, error) {
return nil, nil // 订单不存在
},
}
err := ProcessRefund(repo, 1)
if err == nil {
t.Fatal("期望返回错误,但没有")
}
if err.Error() != "订单不存在" {
t.Errorf("期望 '订单不存在', 实际 '%s'", err.Error())
}
}
func TestProcessRefund_GetOrderError(t *testing.T) {
repo := &mockOrderRepo{
getOrderFunc: func(id int) (*Order, error) {
return nil, errors.New("数据库连接失败")
},
}
err := ProcessRefund(repo, 1)
if err == nil {
t.Fatal("期望返回错误,但没有")
}
}
func TestProcessRefund_NotPaid(t *testing.T) {
repo := &mockOrderRepo{
getOrderFunc: func(id int) (*Order, error) {
return &Order{ID: 1, Status: "cancelled", Amount: 99}, nil
},
}
err := ProcessRefund(repo, 1)
if err == nil {
t.Fatal("期望返回错误,但没有")
}
}
func TestProcessRefund_InvalidAmount(t *testing.T) {
repo := &mockOrderRepo{
getOrderFunc: func(id int) (*Order, error) {
return &Order{ID: 1, Status: "paid", Amount: 0}, nil
},
}
err := ProcessRefund(repo, 1)
if err == nil {
t.Fatal("期望返回错误,但没有")
}
}
5 个测试覆盖了:成功路径、订单不存在、数据库错误、状态不对、金额异常。全是我平时会想的场景,AI 一次性全列出来了。
第二步:执行 + 自动修复循环
type TestRunner struct {
sourceFile string
testFile string
workDir string
maxIterations int
}
func (tr *TestRunner) RunUntilPass(sourceCode string) (string, error) {
// 写入源文件
os.WriteFile(filepath.Join(tr.workDir, tr.sourceFile), []byte(sourceCode), 0644)
for i := 0; i < tr.maxIterations; i++ {
fmt.Printf("第 %d 轮: 生成测试...\n", i+1)
// 1. 分析源码
analysis, err := analyzeSource(sourceCode, tr.sourceFile)
if err != nil {
return "", err
}
// 2. 生成测试
testCode, err := generateTests(sourceCode, tr.sourceFile, analysis)
if err != nil {
return "", err
}
os.WriteFile(filepath.Join(tr.workDir, tr.testFile), []byte(testCode), 0644)
// 3. 执行测试
cmd := exec.Command("go", "test", "-v", "-count=1", "./...")
cmd.Dir = tr.workDir
output, _ := cmd.CombinedOutput()
testOutput := string(output)
if cmd.ProcessState.Success() {
fmt.Println("✅ 全部测试通过!")
return testCode, nil
}
// 4. 分析失败原因
fmt.Printf("❌ 第 %d 轮失败, 分析原因...\n", i+1)
fix := analyzeTestFailure(sourceCode, testCode, testOutput)
// 5. 按需修复
if fix.Type == "source" {
sourceCode = fix.FixedCode
} else {
testCode = fix.FixedCode
}
}
return "", fmt.Errorf("超过最大轮次仍然失败")
}
type FixResult struct {
Type string // "source" 或 "test"
FixedCode string
Reason string
}
func analyzeTestFailure(source, test, output string) *FixResult {
prompt := fmt.Sprintf(`测试失败了。判断是源码的问题还是测试代码的问题,给出修复后的代码。
源码:
%s
测试代码:
%s
测试输出:
%s
如果是源码逻辑有 bug(如边界条件没处理),返回 type: "source" 和修复后的源码。
如果是测试代码写错了(如 mock 不对、assertion 不对),返回 type: "test" 和修复后的测试代码。
如果两者都可能有问题,优先修复源码。
只返回修复后的完整代码。`, source, test, output)
// ... 解析 LLM 响应,返回 FixResult
result, _ := callLLM("你是 Go 调试专家。", prompt)
return parseFixResult(result)
}
实测数据
我把团队一个 1200 行的 service 包丢进去,8 个函数,本来只有 3 个测试(覆盖率 22%)。
| 指标 | 之前 | 之后 |
|---|---|---|
| 测试函数数 | 3 | 28 |
| 代码覆盖率 | 22% | 87% |
| 边界 case 覆盖 | 2 个 | 19 个 |
| AI 迭代次数 | — | 3 轮(2 次自动修复) |
| 耗时 | 手动 3 小时 | 自动 4 分钟 |
4 分钟,87% 覆盖率,19 个边界 case。我手动写至少 3 小时。
什么情况 AI 测试做不好
- 涉及复杂状态机的逻辑。 AI 能枚举单步状态,但多步组合爆炸时覆盖不全。
- 并发代码。
go test -race能检测数据竞争,但 AI 很难写对并发测试。它不理解「goroutine 可能在另一行执行」。 - 需要真实外部环境的。 比如测试「文件上传到 S3」——AI 能帮你写 mock,但真实 S3 的连接超时、重试逻辑它测不了。
- 业务规则隐含在人的脑子的。 AI 只能读代码,不知道你们产品为什么设计成这样。
我的建议
别想着 AI 包办一切。正确姿势是:AI 写 80%(正常+边界),你写 20%(核心业务规则+并发安全)。
有了测试,下一步让它审代码。下一篇搭 Code Review Agent:自动检查安全、性能、风格,像 CI 一样跑在每次 PR 上。
更多推荐



所有评论(0)