ECC 的 Cursor Go 测试规则:从 glob 触发的规则文件到表驱动测试、竞态检测与覆盖率 TDD 工作流
本文以 ECC 仓库中的 Go 测试规则文件 为主线,讲清 ECC 如何通过 Cursor 规则的 glob 作用域机制,把“表驱动测试 + -race 竞态检测 + 覆盖率验证”的 Go 测试约定注入 AI 编码代理;并结合该规则引用的 golang-testing 技能、通用测试规则 与 /go-test 命令,完整还原一套可直接落地的 Go 测试工程化实践。
规则文件的定位与触发机制
ECC 的 .cursor/rules/ 目录采用“通用规则 + 语言专属规则”的两层结构。通用层由 common-testing.md、common-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/op 与 allocs/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) == 0、Compare(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 生成测试代码的质量契约。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00