首页
/ ECC 的 Cursor Go 测试规则:从 glob 触发的规则文件到表驱动测试、竞态检测与覆盖率 TDD 工作流

ECC 的 Cursor Go 测试规则:从 glob 触发的规则文件到表驱动测试、竞态检测与覆盖率 TDD 工作流

2026-09-06 14:17:43作者:蔡怀权

本文以 ECC 仓库中的 Go 测试规则文件 为主线,讲清 ECC 如何通过 Cursor 规则的 glob 作用域机制,把“表驱动测试 + -race 竞态检测 + 覆盖率验证”的 Go 测试约定注入 AI 编码代理;并结合该规则引用的 golang-testing 技能通用测试规则/go-test 命令,完整还原一套可直接落地的 Go 测试工程化实践。

规则文件的定位与触发机制

ECC 的 .cursor/rules/ 目录采用“通用规则 + 语言专属规则”的两层结构。通用层由 common-testing.mdcommon-coding-style.md 等文件组成(alwaysApply: true,始终生效);语言层则为每种语言提供 coding-style、hooks、patterns、security、testing 五个维度的规则文件,Go 对应的是 golang-* 前缀的五个文件。

golang-testing.md 的完整内容如下:

---
description: "Go testing extending common rules"
globs: ["**/*.go", "**/go.mod", "**/go.sum"]
alwaysApply: false
---
# Go Testing

> This file extends the common testing rule with Go specific content.

## Framework

Use the standard `go test` with **table-driven tests**.

## Race Detection

Always run with the `-race` flag:

```bash
go test -race ./...

Coverage

go test -cover ./...

Reference

See skill: golang-testing for detailed Go testing patterns and helpers.


从源码结构看,这个规则文件的设计意图非常清晰:

1. **`alwaysApply: false` + `globs` 精确触发**:该规则不全局注入,只在代理处理 `**/*.go`、`**/go.mod`、`**/go.sum` 三类文件时生效,避免非 Go 上下文浪费上下文预算。同样的 glob 配置也出现在同目录的 [golang-patterns.md](https://gitcode.com/GitHub_Trending/ev/ECC/blob/22e8cf01d0b54719b3a49002fab2ccbda4ff5b9e/.cursor/rules/golang-patterns.md?utm_source=gitcode_repo_files)、[golang-hooks.md](https://gitcode.com/GitHub_Trending/ev/ECC/blob/22e8cf01d0b54719b3a49002fab2ccbda4ff5b9e/.cursor/rules/golang-hooks.md?utm_source=gitcode_repo_files)、[golang-coding-style.md](https://gitcode.com/GitHub_Trending/ev/ECC/blob/22e8cf01d0b54719b3a49002fab2ccbda4ff5b9e/.cursor/rules/golang-coding-style.md?utm_source=gitcode_repo_files) 中,构成一致的触发面。
2. **“extends common rule” 的继承关系**:文件开头的引用声明表明它是对通用测试规则的增量扩展。真正的硬性指标定义在 [common-testing.md](https://gitcode.com/GitHub_Trending/ev/ECC/blob/22e8cf01d0b54719b3a49002fab2ccbda4ff5b9e/.cursor/rules/common-testing.md?utm_source=gitcode_repo_files) 中——80% 最低覆盖率、单元/集成/E2E 三类测试缺一不可、强制 RED-GREEN-REFACTOR 的 TDD 工作流,以及失败时优先使用 `tdd-guide` 代理排障(检查测试隔离、验证 mock 正确性、修实现而非改测试)。
3. **规则只是“指针”,技能承载深度**:规则正文保持极简(框架选择、`-race`、`-cover`、一个引用),把细节下沉到 [skills/golang-testing/SKILL.md](https://gitcode.com/GitHub_Trending/ev/ECC/blob/22e8cf01d0b54719b3a49002fab2ccbda4ff5b9e/skills/golang-testing/SKILL.md?utm_source=gitcode_repo_files)。这是 ECC 典型的“规则管约束、技能管模式”的分工。

## 三大核心约束

### 框架:标准 `go test` + 表驱动测试

规则明确要求使用 Go 标准测试框架并采用**表驱动测试(table-driven tests)**。这一约定与 ECC 的 Go 编码风格一脉相承——[golang-coding-style.md](https://gitcode.com/GitHub_Trending/ev/ECC/blob/22e8cf01d0b54719b3a49002fab2ccbda4ff5b9e/.cursor/rules/golang-coding-style.md?utm_source=gitcode_repo_files) 中 gofmt/goimports 是强制项、错误必须用 `%w` 包装,测试侧的表驱动结构则是断言组织方式的统一答案。

### 竞态检测:始终带 `-race`

```bash
go test -race ./...

规则用 “Always” 一词将 -race 定为不可选要求。这与 golang-testing 技能 的 CI 章节呼应:CI 示例中运行命令是 go test -race -coverprofile=coverage.out ./...,即竞态检测与覆盖率采集在持续集成中同时启用,两者缺一不可。

覆盖率:go test -cover ./...

go test -cover ./...

这条命令对应通用规则中 80% 的覆盖率底线(见 common-testing.md)。技能文件中给出了更完整的命令集与分级目标,见下文“覆盖率命令与目标”一节。

深入 golang-testing 技能:TDD 工作流与核心测试模式

RED-GREEN-REFACTOR 循环

技能文档 定义了 Go 下的 TDD 循环,并与 ECC 的强制 TDD 要求(common-testing.md 中 MANDATORY workflow)一致:

RED     → Write a failing test first
GREEN   → Write minimal code to pass the test
REFACTOR → Improve code while keeping tests green
REPEAT  → Continue with next requirement

技能中的标准步骤是:先定义函数签名(用 panic("not implemented") 占位)→ 写失败测试 → 运行确认 FAIL → 写最小实现 → 运行确认 PASS → 保持测试绿色做重构。

表驱动测试:基础版与错误用例版

基础表驱动模式,用最少代码覆盖多组输入:

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -1, -2, -3},
        {"zero values", 0, 0, 0},
        {"mixed signs", -1, 1, 0},
        {"large numbers", 1000000, 2000000, 3000000},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, got, tt.expected)
            }
        })
    }
}

错误路径用 wantErr bool 字段表达,先判错再比对值,错误时提前 return

func TestParseConfig(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    *Config
        wantErr bool
    }{
        {
            name:  "valid config",
            input: `{"host": "localhost", "port": 8080}`,
            want:  &Config{Host: "localhost", Port: 8080},
        },
        {
            name:    "invalid JSON",
            input:   `{invalid}`,
            wantErr: true,
        },
        {
            name:    "empty input",
            input:   "",
            wantErr: true,
        },
        {
            name:  "minimal config",
            input: `{}`,
            want:  &Config{}, // Zero value config
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := ParseConfig(tt.input)

            if tt.wantErr {
                if err == nil {
                    t.Error("expected error, got nil")
                }
                return
            }

            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }

            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("got %+v; want %+v", got, tt.want)
            }
        })
    }
}

这个“先短路错误分支、再断言值”的结构,正好落实了技能最佳实践中“Skip error path testing 属于 DON'T”一条——错误路径是一等公民。

子测试与并行执行

t.Run 组织同一资源的 CRUD 生命周期,共享父级 setup:

func TestUser(t *testing.T) {
    // Setup shared by all subtests
    db := setupTestDB(t)

    t.Run("Create", func(t *testing.T) {
        user := &User{Name: "Alice"}
        err := db.CreateUser(user)
        if err != nil {
            t.Fatalf("CreateUser failed: %v", err)
        }
        if user.ID == "" {
            t.Error("expected user ID to be set")
        }
    })

    t.Run("Get", func(t *testing.T) {
        user, err := db.GetUser("alice-id")
        if err != nil {
            t.Fatalf("GetUser failed: %v", err)
        }
        if user.Name != "Alice" {
            t.Errorf("got name %q; want %q", user.Name, "Alice")
        }
    })

    t.Run("Update", func(t *testing.T) {
        // ...
    })

    t.Run("Delete", func(t *testing.T) {
        // ...
    })
}

并行子测试的关键是捕获循环变量 + t.Parallel()

for _, tt := range tests {
    tt := tt // Capture range variable
    t.Run(tt.name, func(t *testing.T) {
        t.Parallel() // Run subtests in parallel
        result := Process(tt.input)
        // assertions...
        _ = result
    })
}

注意 tt := tt 的写法针对旧版 Go 的循环变量语义;在 Go 1.22+ 中循环变量默认按次新建,该语句可省略——这是阅读示例代码时的一个适用前提。

测试辅助函数:t.Helper、t.Cleanup、t.TempDir、泛型断言

func setupTestDB(t *testing.T) *sql.DB {
    t.Helper() // Marks this as a helper function

    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        t.Fatalf("failed to open database: %v", err)
    }

    // Cleanup when test finishes
    t.Cleanup(func() {
        db.Close()
    })

    // Run migrations
    if _, err := db.Exec(schema); err != nil {
        t.Fatalf("failed to create schema: %v", err)
    }

    return db
}

func assertNoError(t *testing.T, err error) {
    t.Helper()
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
}

func assertEqualT comparable {
    t.Helper()
    if got != want {
        t.Errorf("got %v; want %v", got, want)
    }
}

三个机制各有分工:t.Helper() 让失败堆栈指向真正的调用方而非辅助函数;t.Cleanup 在测试结束后自动执行资源释放;t.TempDir() 创建自动清理的临时目录(技能中用它演示 ProcessFile 的文件 IO 测试)。assertEqual 则展示了 Go 1.18+ 泛型在测试工具中的用法。

黄金文件(Golden Files)测试

对输出内容做逐字节比对,期望值存放在 testdata/ 目录,并支持 -update 标志重生成:

var update = flag.Bool("update", false, "update golden files")

func TestRender(t *testing.T) {
    tests := []struct {
        name  string
        input Template
    }{
        {"simple", Template{Name: "test"}},
        {"complex", Template{Name: "test", Items: []string{"a", "b"}}},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Render(tt.input)

            golden := filepath.Join("testdata", tt.name+".golden")

            if *update {
                // Update golden file: go test -update
                err := os.WriteFile(golden, got, 0644)
                if err != nil {
                    t.Fatalf("failed to update golden file: %v", err)
                }
            }

            want, err := os.ReadFile(golden)
            if err != nil {
                t.Fatalf("failed to read golden file: %v", err)
            }

            if !bytes.Equal(got, want) {
                t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)
            }
        })
    }
}

基于接口的 Mock

golang-patterns.md 中“小接口 + 构造函数注入”的原则配套,Mock 实现只依赖接口字段中的函数:

// Define interface for dependencies
type UserRepository interface {
    GetUser(id string) (*User, error)
    SaveUser(user *User) error
}

// Mock implementation for tests
type MockUserRepository struct {
    GetUserFunc  func(id string) (*User, error)
    SaveUserFunc func(user *User) error
}

func (m *MockUserRepository) GetUser(id string) (*User, error) {
    return m.GetUserFunc(id)
}

func (m *MockUserRepository) SaveUser(user *User) error {
    return m.SaveUserFunc(user)
}

// Test using mock
func TestUserService(t *testing.T) {
    mock := &MockUserRepository{
        GetUserFunc: func(id string) (*User, error) {
            if id == "123" {
                return &User{ID: "123", Name: "Alice"}, nil
            }
            return nil, ErrNotFound
        },
    }

    service := NewUserService(mock)

    user, err := service.GetUserProfile("123")
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if user.Name != "Alice" {
        t.Errorf("got name %q; want %q", user.Name, "Alice")
    }
}

技能的最佳实践明确提醒“DON'T: Mock everything(prefer integration tests when possible)”——Mock 只用于隔离真正的重依赖。

基准测试与模糊测试

基准测试:ResetTimer、多规模子基准、内存分配对比

func BenchmarkProcess(b *testing.B) {
    data := generateTestData(1000)
    b.ResetTimer() // Don't count setup time

    for i := 0; i < b.N; i++ {
        Process(data)
    }
}

// Run: go test -bench=BenchmarkProcess -benchmem
// Output: BenchmarkProcess-8   10000   105234 ns/op   4096 B/op   10 allocs/op

按数据规模划分子基准时,要在每次迭代内复制数据,避免反复排序已经有序的数据:

func BenchmarkSort(b *testing.B) {
    sizes := []int{100, 1000, 10000, 100000}

    for _, size := range sizes {
        b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
            data := generateRandomSlice(size)
            b.ResetTimer()

            for i := 0; i < b.N; i++ {
                // Make a copy to avoid sorting already sorted data
                tmp := make([]int, len(data))
                copy(tmp, data)
                sort.Ints(tmp)
            }
        })
    }
}

对比不同实现的内存分配(+ 拼接 vs strings.Builder vs strings.Join)是技能给出的典型模板,配合 -benchmem 输出 B/opallocs/op 两列数据。

模糊测试(Go 1.18+)

种子语料 + 不变量断言是技能中 Fuzz 的两大要素:

func FuzzParseJSON(f *testing.F) {
    // Add seed corpus
    f.Add(`{"name": "test"}`)
    f.Add(`{"count": 123}`)
    f.Add(`[]`)
    f.Add(`""`)

    f.Fuzz(func(t *testing.T, input string) {
        var result map[string]interface{}
        err := json.Unmarshal([]byte(input), &result)

        if err != nil {
            // Invalid JSON is expected for random input
            return
        }

        // If parsing succeeded, re-encoding should work
        _, err = json.Marshal(result)
        if err != nil {
            t.Errorf("Marshal failed after successful Unmarshal: %v", err)
        }
    })
}

// Run: go test -fuzz=FuzzParseJSON -fuzztime=30s

多输入 Fuzz 演示了比较函数的性质断言:Compare(a, a) == 0Compare(a, b)Compare(b, a) 符号相反。

覆盖率:命令、目标与生成代码排除

完整命令集

# Basic coverage
go test -cover ./...

# Generate coverage profile
go test -coverprofile=coverage.out ./...

# View coverage in browser
go tool cover -html=coverage.out

# View coverage by function
go tool cover -func=coverage.out

# Coverage with race detection
go test -race -coverprofile=coverage.out ./...

分级覆盖率目标

技能中的目标表/go-test 命令 中完全一致):

代码类型 目标
关键业务逻辑 100%
公开 API 90%+
一般代码 80%+
生成代码 排除

80% 的底线与 common-testing.md 中 “Minimum Test Coverage: 80%” 呼应;生成代码可通过 build tags 排除,例如 go test -cover -tags=!generate ./...

HTTP 处理器测试与完整测试命令参考

httptest 表驱动测试

技能给出了单用例与表驱动两种 HTTP 测试写法,核心是 httptest.NewRequest + httptest.NewRecorder 的组合:

func TestAPIHandler(t *testing.T) {
    tests := []struct {
        name       string
        method     string
        path       string
        body       string
        wantStatus int
        wantBody   string
    }{
        {
            name:       "get user",
            method:     http.MethodGet,
            path:       "/users/123",
            wantStatus: http.StatusOK,
            wantBody:   `{"id":"123","name":"Alice"}`,
        },
        {
            name:       "not found",
            method:     http.MethodGet,
            path:       "/users/999",
            wantStatus: http.StatusNotFound,
        },
        {
            name:       "create user",
            method:     http.MethodPost,
            path:       "/users",
            body:       `{"name":"Bob"}`,
            wantStatus: http.StatusCreated,
        },
    }

    handler := NewAPIHandler()

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            var body io.Reader
            if tt.body != "" {
                body = strings.NewReader(tt.body)
            }

            req := httptest.NewRequest(tt.method, tt.path, body)
            req.Header.Set("Content-Type", "application/json")
            w := httptest.NewRecorder()

            handler.ServeHTTP(w, req)

            if w.Code != tt.wantStatus {
                t.Errorf("got status %d; want %d", w.Code, tt.wantStatus)
            }

            if tt.wantBody != "" && w.Body.String() != tt.wantBody {
                t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody)
            }
        })
    }
}

全量命令速查

# Run all tests
go test ./...

# Run tests with verbose output
go test -v ./...

# Run specific test
go test -run TestAdd ./...

# Run tests matching pattern
go test -run "TestUser/Create" ./...

# Run tests with race detector
go test -race ./...

# Run tests with coverage
go test -cover -coverprofile=coverage.out ./...

# Run short tests only
go test -short ./...

# Run tests with timeout
go test -timeout 30s ./...

# Run benchmarks
go test -bench=. -benchmem ./...

# Run fuzzing
go test -fuzz=FuzzParse -fuzztime=30s ./...

# Count test runs (for flaky test detection)
go test -count=10 ./...

其中 -short 对应 testing.Short() 开关(跳过耗时的集成逻辑),-count=10 用于暴露 flaky 测试——而技能最佳实践明确要求“忽略 flaky 测试属于 DON'T,应修复或删除”。

最佳实践与 CI/CD 集成

技能文档的 DO / DON'T 清单:

DO: 先写测试(TDD);用表驱动测试做全面覆盖;测行为而非实现细节;辅助函数加 t.Helper();独立测试用 t.Parallel();用 t.Cleanup() 清理资源;测试名描述场景。

DON'T: 直接测试私有函数(走公共 API);在测试里 time.Sleep()(改用 channel 或条件);忽视 flaky 测试;无差别 Mock;跳过错误路径。

CI 侧的技能给出 GitHub Actions 示例(go-version 1.22),把 -race、覆盖率采集与 80% 门槛串联为门禁:

test:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-go@v5
      with:
        go-version: '1.22'

    - name: Run tests
      run: go test -race -coverprofile=coverage.out ./...

    - name: Check coverage
      run: |
        go tool cover -func=coverage.out | grep total | awk '{print $3}' | \
        awk -F'%' '{if ($1 < 80) exit 1}'

这段 YAML 把规则文件里“Always run with the -race flag”的口头约束变成了可执行的流水线门禁。

与 ECC 其他 Go 规则的联动

单看 golang-testing.md 只有三条约束,但它与同目录规则形成闭环:

  • /go-test 命令:规则管“被动约束”,命令管“主动执行”。/go-test 以 TDD 会话驱动整个流程——定义签名 → 写表驱动测试(RED)→ 跑测确认失败 → 最小实现(GREEN)→ 重构 → 检查 80%+ 覆盖率,其中邮箱校验器示例展示了完整的六步演示;它还与 /go-review(实现后评审)、verification-loop 技能衔接。
  • golang-hooks.md:在 ~/.claude/settings.json 中配置 PostToolUse hooks,编辑 .go 文件后自动跑 gofmt/goimports、go vet、staticcheck——测试规则的产物在合入前就经过静态检查。
  • golang-patterns.md / golang-coding-style.md:构造函数注入依赖的写法正是接口 Mock 能成立的前提;错误用 %w 包装则让测试中的 errors.Is/errors.As 断言有意义。
  • tdd-guide 代理:common-testing.md 指定测试失败时主动使用该代理排障。

适用前提与限制

  • ECC 仓库本身是一个“规则 + 技能 + 命令”的代理工程化仓库,当前仓库内没有 .go 源文件;上述所有 Go 代码均为规则/技能文档中的示例模式,用于指导 AI 代理在你自己的 Go 项目中生成的测试代码。
  • 模糊测试要求 Go 1.18+;CI 示例固定 Go 1.22;tt := tt 的循环变量捕获写法在 Go 1.22+ 已非必需。
  • -race 依赖 CGO 支持的环境(Linux、macOS 开箱即用;部分平台受限),跨平台 CI 时需留意。

小结

golang-testing.md 用不到 30 行定义了 ECC 对 Go 测试的三条硬约束——标准 go test + 表驱动、永远 -race-cover 验证覆盖率——并通过 globs 精确限定在 Go 文件作用域内触发。它向上继承 common-testing.md 的 80% 覆盖率与强制 TDD,向下由 skills/golang-testing/SKILL.md 提供从表驱动、子测试、辅助函数、黄金文件、接口 Mock 到基准测试、Fuzz 与 CI 门禁的完整模式库,再经 /go-test 命令 变成可被代理直接执行的工作流。对使用 ECC 的 Go 项目而言,这套三层结构(规则定底线、技能给模式、命令跑流程)就是 AI 生成测试代码的质量契约。

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