我在上家公司写过单元测试覆盖率要求——“新增代码覆盖率不低于 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 测试做不好

  1. 涉及复杂状态机的逻辑。 AI 能枚举单步状态,但多步组合爆炸时覆盖不全。
  2. 并发代码。 go test -race 能检测数据竞争,但 AI 很难写对并发测试。它不理解「goroutine 可能在另一行执行」。
  3. 需要真实外部环境的。 比如测试「文件上传到 S3」——AI 能帮你写 mock,但真实 S3 的连接超时、重试逻辑它测不了。
  4. 业务规则隐含在人的脑子的。 AI 只能读代码,不知道你们产品为什么设计成这样。

我的建议

别想着 AI 包办一切。正确姿势是:AI 写 80%(正常+边界),你写 20%(核心业务规则+并发安全)。

有了测试,下一步让它审代码。下一篇搭 Code Review Agent:自动检查安全、性能、风格,像 CI 一样跑在每次 PR 上。

更多推荐