Go 单元测试 Skill
描述
这个 skill 帮助开发者使用 testify 库为 Go 代码生成和维护单元测试。当 AI 生成或修改 Go 代码后,会自动为重要逻辑生成或更新单元测试,确保代码质量和测试覆盖率。
何时使用
在以下场景中使用这个 skill:
- AI 生成或修改了 Go 代码后,需要为重要逻辑生成单元测试
- 用户需要为现有代码编写单元测试
- 用户需要提高代码的测试覆盖率
- 用户需要确保代码逻辑的正确性
- 用户询问如何使用 testify 编写单元测试
- 用户需要更新现有测试用例以匹配代码变更
前置要求
在使用 testify 之前,确保满足以下依赖:
- Golang: >= 1.19
- testify: 最新版本(通过
go get安装) - 测试环境: 能够运行
go test命令
安装 testify
安装步骤
在项目中使用 testify:
go get github.com/stretchr/testify
验证安装
go list -m github.com/stretchr/testify
应该显示 testify 的版本信息。
AI 交互指导
⚠️ 重要:自动生成和维护单元测试
关键原则:在每次生成或修改 Go 代码后,AI 必须:
- 识别重要逻辑 - 分析代码,识别需要测试的重要函数、方法和业务逻辑
- 检查现有测试 - 检查是否已存在对应的测试文件
- 生成或更新测试 - 为重要逻辑生成新的测试用例或更新现有测试
- 使用 testify - 使用 testify 的
assert和require包编写测试 - 运行测试 - 执行
go test验证测试通过 - 确保覆盖率 - 确保关键逻辑有足够的测试覆盖
禁止行为:
- ❌ 禁止跳过单元测试生成
- ❌ 禁止忽略测试失败
- ❌ 禁止在修改代码后不更新相关测试
- ❌ 禁止只测试简单逻辑而忽略复杂业务逻辑
1. 标准工作流程
当 AI 生成或修改 Go 代码后,必须遵循以下流程:
- 分析代码 - 识别需要测试的重要函数和方法
- 检查测试文件 - 检查是否存在对应的
*_test.go文件 - 生成测试代码 - 使用 testify 生成测试用例
- 运行测试 - 执行
go test -v ./...或针对特定包的测试 - 修复测试 - 如果测试失败,修复测试代码或业务代码
- 验证覆盖率 - 运行
go test -cover检查测试覆盖率 - 报告结果 - 向用户报告测试生成和运行的结果
2. 重要逻辑识别标准
以下类型的代码必须生成单元测试:
- 业务核心逻辑 - 包含业务规则和计算逻辑的函数
- 数据处理函数 - 数据转换、验证、格式化等函数
- 错误处理逻辑 - 错误判断和处理流程
- 边界条件处理 - 处理边界值和异常情况的代码
- 公共 API - 对外暴露的接口和函数
- 复杂算法 - 包含复杂计算或逻辑判断的代码
- 状态转换 - 涉及状态变化的代码
以下类型的代码可以跳过测试(但建议测试):
- 简单的 getter/setter 方法
- 简单的常量定义
- 简单的类型定义(无方法)
3. 执行示例
场景 1:生成新代码后(以下为假设示例,实际函数名和路径视具体项目而定)
[AI 生成业务逻辑代码]
分析代码,识别需要测试的重要逻辑:
- 函数 <实际函数名1>: <逻辑描述>
- 函数 <实际函数名2>: <逻辑描述>
- 方法 <实际方法名>: <逻辑描述>
检查测试文件...
[检查: 是否存在对应的 *_test.go 文件]
生成单元测试...
[生成: <对应文件>_test.go]
运行测试验证...
[执行: go test -v <对应包路径>]
✅ 测试通过!覆盖了以下场景:
- <实际函数名1>: 正常计算、边界值等
- <实际函数名2>: 有效输入、无效输入等
- <实际方法名>: 成功处理、失败处理等
场景 2:修改现有代码后(假设示例)
[AI 修改业务逻辑代码]
检查现有测试文件...
[检查: <对应文件>_test.go 是否存在]
更新测试用例以匹配代码变更...
[更新: <对应文件>_test.go 中的相关测试]
运行测试验证...
[执行: go test -v <对应包路径>]
✅ 所有测试通过!已更新以下测试:
- Test<实际函数名1>: 新增场景测试
- Test<实际方法名>: 更新处理流程测试
场景 3:用户明确要求生成测试
用户:"为 user_service.go 生成单元测试"
分析代码结构...
[分析: user_service.go 中的函数和方法]
生成完整的测试文件...
[生成: user_service_test.go]
运行测试验证...
[执行: go test -v -run TestUserService]
✅ 测试生成完成!包含以下测试用例:
- TestCreateUser: 创建用户成功/失败场景
- TestGetUser: 获取用户成功/不存在场景
- TestUpdateUser: 更新用户成功/失败场景
- TestDeleteUser: 删除用户成功/失败场景
4. 测试文件命名规范
- 测试文件必须与被测试文件在同一包中
- 测试文件命名:
<被测试文件名>_test.go - 例如:
user_service.go的测试文件为user_service_test.go
5. 测试函数命名规范
- 测试函数必须以
Test开头 - 测试函数名应该描述测试的场景
- 格式:
Test<函数名>_<场景描述> - 例如:
TestCalculateTotal_WithDiscount,TestValidateUser_InvalidEmail
testify 使用指南
assert 包
assert 包提供断言方法,失败时不会终止测试:
package yours
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSomething(t *testing.T) {
// 断言相等
assert.Equal(t, 123, 123, "they should be equal")
// 断言不相等
assert.NotEqual(t, 123, 456, "they should not be equal")
// 断言为 nil(用于错误检查)
assert.Nil(t, err)
// 断言不为 nil
if assert.NotNil(t, object) {
// 现在可以安全地进行进一步断言
assert.Equal(t, "Something", object.Value)
}
// 断言为真
assert.True(t, condition, "condition should be true")
// 断言为假
assert.False(t, condition, "condition should be false")
// 断言包含
assert.Contains(t, "Hello World", "World")
// 断言长度
assert.Len(t, slice, 3, "slice should have length 3")
}
使用 assert 实例(推荐)
如果有很多断言,可以创建 assert 实例:
func TestSomething(t *testing.T) {
assert := assert.New(t)
// 不需要传入 t 参数
assert.Equal(123, 123, "they should be equal")
assert.NotEqual(123, 456, "they should not be equal")
assert.Nil(err)
if assert.NotNil(object) {
assert.Equal("Something", object.Value)
}
}
require 包
require 包提供与 assert 相同的函数,但失败时会立即终止测试:
package yours
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestSomething(t *testing.T) {
// 如果失败,测试会立即终止
require.Equal(t, 123, 123, "they should be equal")
require.NotNil(t, object, "object should not be nil")
// 后续代码只有在前面的断言通过时才会执行
result := object.DoSomething()
require.Equal(t, "expected", result)
}
何时使用 require:
- 当后续测试依赖前置条件时使用
require - 当断言失败后继续测试没有意义时使用
require - 其他情况使用
assert,允许测试继续执行以发现更多问题
mock 包
mock 包用于创建 mock 对象,用于测试依赖外部服务的代码:
package yours
import (
"testing"
"github.com/stretchr/testify/mock"
)
// 定义 mock 对象
type MockUserRepository struct {
mock.Mock
}
// 实现接口方法
func (m *MockUserRepository) GetUser(id int) (*User, error) {
args := m.Called(id)
return args.Get(0).(*User), args.Error(1)
}
func TestUserService(t *testing.T) {
// 创建 mock 对象
mockRepo := new(MockUserRepository)
// 设置期望
expectedUser := &User{ID: 1, Name: "John"}
mockRepo.On("GetUser", 1).Return(expectedUser, nil)
// 使用 mock 对象
service := NewUserService(mockRepo)
user, err := service.GetUser(1)
// 断言结果
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)
// 验证 mock 期望被调用
mockRepo.AssertExpectations(t)
}
suite 包
suite 包用于创建测试套件,支持 setup/teardown:
package yours
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type ExampleTestSuite struct {
suite.Suite
VariableThatShouldStartAtFive int
}
// 每个测试前的设置
func (suite *ExampleTestSuite) SetupTest() {
suite.VariableThatShouldStartAtFive = 5
}
// 每个测试后的清理
func (suite *ExampleTestSuite) TearDownTest() {
// 清理代码
}
// 测试用例
func (suite *ExampleTestSuite) TestExample() {
suite.Equal(5, suite.VariableThatShouldStartAtFive)
}
// 运行测试套件
func TestExampleTestSuite(t *testing.T) {
suite.Run(t, new(ExampleTestSuite))
}
测试用例编写规范
1. 测试结构
每个测试函数应该遵循 AAA 模式(Arrange-Act-Assert):
func TestCalculateTotal_WithDiscount(t *testing.T) {
// Arrange: 准备测试数据
items := []Item{
{Price: 100, Quantity: 2},
{Price: 50, Quantity: 1},
}
discount := 0.1
// Act: 执行被测试的函数
total := CalculateTotal(items, discount)
// Assert: 验证结果
expected := float64(225) // (100*2 + 50*1) * 0.9
assert.Equal(t, expected, total)
}
2. 测试场景覆盖
每个函数应该测试以下场景:
- 正常场景 - 正常输入和预期输出
- 边界场景 - 边界值、空值、零值
- 异常场景 - 错误输入、异常情况
- 错误处理 - 错误返回和错误信息
示例:
func TestValidateUser(t *testing.T) {
t.Run("ValidUser", func(t *testing.T) {
user := &User{Name: "John", Email: "john@example.com"}
err := ValidateUser(user)
assert.NoError(t, err)
})
t.Run("EmptyName", func(t *testing.T) {
user := &User{Name: "", Email: "john@example.com"}
err := ValidateUser(user)
assert.Error(t, err)
assert.Contains(t, err.Error(), "name")
})
t.Run("InvalidEmail", func(t *testing.T) {
user := &User{Name: "John", Email: "invalid-email"}
err := ValidateUser(user)
assert.Error(t, err)
assert.Contains(t, err.Error(), "email")
})
t.Run("NilUser", func(t *testing.T) {
err := ValidateUser(nil)
assert.Error(t, err)
})
}
3. 使用表驱动测试
对于多个相似场景,使用表驱动测试:
func TestCalculateDiscount(t *testing.T) {
tests := []struct {
name string
originalPrice float64
discountRate float64
expected float64
expectError bool
}{
{
name: "正常折扣",
originalPrice: 100,
discountRate: 0.1,
expected: 90,
expectError: false,
},
{
name: "无折扣",
originalPrice: 100,
discountRate: 0,
expected: 100,
expectError: false,
},
{
name: "负折扣率",
originalPrice: 100,
discountRate: -0.1,
expected: 0,
expectError: true,
},
{
name: "折扣率超过1",
originalPrice: 100,
discountRate: 1.5,
expected: 0,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := CalculateDiscount(tt.originalPrice, tt.discountRate)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}
4. Mock 外部依赖
使用 mock 对象隔离外部依赖:
func TestUserService_CreateUser(t *testing.T) {
t.Run("成功创建用户", func(t *testing.T) {
// 创建 mock repository
mockRepo := new(MockUserRepository)
// 设置期望
expectedUser := &User{ID: 1, Name: "John", Email: "john@example.com"}
mockRepo.On("Create", mock.AnythingOfType("*User")).Return(expectedUser, nil)
mockRepo.On("Exists", "john@example.com").Return(false, nil)
// 创建 service
service := NewUserService(mockRepo)
// 执行测试
user, err := service.CreateUser("John", "john@example.com")
// 验证结果
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)
mockRepo.AssertExpectations(t)
})
t.Run("邮箱已存在", func(t *testing.T) {
mockRepo := new(MockUserRepository)
mockRepo.On("Exists", "john@example.com").Return(true, nil)
service := NewUserService(mockRepo)
user, err := service.CreateUser("John", "john@example.com")
assert.Error(t, err)
assert.Nil(t, user)
assert.Contains(t, err.Error(), "already exists")
mockRepo.AssertExpectations(t)
})
}
运行测试
基本测试命令
# 运行当前包的所有测试
go test
# 运行测试并显示详细信息
go test -v
# 运行特定测试函数
go test -v -run TestFunctionName
# 运行测试并显示覆盖率
go test -cover
# 运行测试并生成覆盖率报告
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
运行特定包的测试
# 运行特定包的测试
go test ./internal/service
# 运行所有包的测试
go test ./...
# 运行所有包的测试并显示覆盖率
go test -cover ./...
测试覆盖率
# 查看覆盖率
go test -cover ./...
# 生成详细的覆盖率报告
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
# 生成 HTML 覆盖率报告
go tool cover -html=coverage.out
常用测试场景
1. 测试 HTTP Handler
func TestUserHandler_GetUser(t *testing.T) {
// 创建 mock service
mockService := new(MockUserService)
expectedUser := &User{ID: 1, Name: "John"}
mockService.On("GetUser", 1).Return(expectedUser, nil)
// 创建 handler
handler := NewUserHandler(mockService)
// 创建 HTTP 请求
req := httptest.NewRequest("GET", "/users/1", nil)
w := httptest.NewRecorder()
// 执行 handler
handler.GetUser(w, req)
// 验证响应
assert.Equal(t, http.StatusOK, w.Code)
var response UserResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, expectedUser.ID, response.ID)
mockService.AssertExpectations(t)
}
2. 测试数据库操作
func TestUserRepository_Create(t *testing.T) {
// 使用测试数据库(如 sqlmock)
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
repo := NewUserRepository(db)
// 设置 mock 期望
mock.ExpectExec("INSERT INTO users").
WithArgs("John", "john@example.com").
WillReturnResult(sqlmock.NewResult(1, 1))
// 执行测试
user, err := repo.Create(&User{Name: "John", Email: "john@example.com"})
// 验证结果
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, 1, user.ID)
// 验证所有期望都被满足
assert.NoError(t, mock.ExpectationsWereMet())
}
3. 测试并发安全
func TestCounter_Increment_Concurrent(t *testing.T) {
counter := NewCounter()
goroutines := 100
iterations := 1000
var wg sync.WaitGroup
wg.Add(goroutines)
// 启动多个 goroutine 并发增加计数
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
counter.Increment()
}
}()
}
wg.Wait()
// 验证最终值
expected := goroutines * iterations
assert.Equal(t, expected, counter.Value())
}
集成到开发流程
在 CI/CD 中集成
⚠️ 重要限制:除非用户主动明确提出需要在 CI/CD 中集成测试,否则绝对不要擅自修改或创建 .gitlab-ci.yml 或其他 CI/CD 配置文件。仅当用户要求时,才参考以下配置:
# .gitlab-ci.yml 示例
test:
stage: test
script:
- go test -v -coverprofile=coverage.out ./...
- go tool cover -func=coverage.out
coverage: '/total:\s+(\d+\.\d+)%/'
only:
- merge_requests
- main
在 Makefile 中集成
在项目的 Makefile 中添加测试命令:
.PHONY: test
test:
go test -v ./...
.PHONY: test-cover
test-cover:
go test -v -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
.PHONY: test-cover-html
test-cover-html:
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
工作流程示例
典型的新功能开发流程
- AI 生成业务代码 - 根据用户需求生成新的 Go 代码
- 识别重要逻辑 - AI 分析代码,识别需要测试的函数和方法
- 生成测试代码 - AI 使用 testify 生成单元测试
- 运行测试 - AI 执行
go test -v验证测试 - 修复问题 - 如果测试失败,修复测试或业务代码
- 验证覆盖率 - AI 运行
go test -cover检查覆盖率 - 提交代码 - 用户确认后提交代码和测试
代码修改流程
- AI 修改业务代码 - 根据用户需求修改现有代码
- 检查相关测试 - AI 检查是否存在对应的测试文件
- 更新测试用例 - AI 更新测试以匹配代码变更
- 运行测试验证 - AI 执行测试确保所有测试通过
- 添加新场景测试 - 如果代码变更引入了新场景,添加相应测试
故障排查
问题:testify 未安装
症状:导入 testify 时出现错误
解决方案:
go get github.com/stretchr/testify
问题:测试失败
症状:运行测试时出现失败
解决方案:
- 仔细阅读错误信息
- 检查测试断言是否正确
- 检查 mock 对象的期望设置
- 验证测试数据是否正确
问题:测试覆盖率低
症状:覆盖率报告显示覆盖率较低
解决方案:
- 识别未覆盖的代码路径
- 为重要逻辑添加测试用例
- 使用
go tool cover -html=coverage.out查看详细覆盖情况 - 优先覆盖业务核心逻辑
问题:Mock 对象未按预期工作
症状:Mock 对象的期望未被满足
解决方案:
- 检查
On()方法的参数是否匹配实际调用 - 使用
mock.Anything或mock.MatchedBy()处理复杂参数 - 确保调用了
AssertExpectations(t) - 检查 mock 对象是否正确传递到被测试代码
最佳实践
- 测试命名清晰 - 测试函数名应该清楚描述测试的场景
- 一个测试一个场景 - 每个测试函数只测试一个场景
- 使用表驱动测试 - 对于多个相似场景,使用表驱动测试
- 隔离外部依赖 - 使用 mock 对象隔离外部依赖
- 测试边界条件 - 确保测试边界值和异常情况
- 保持测试简单 - 测试代码应该简单易懂
- 及时更新测试 - 修改代码后及时更新相关测试
- 关注覆盖率 - 确保重要逻辑有足够的测试覆盖
相关资源
- testify 官方文档: https://github.com/stretchr/testify
- Go 测试文档: https://go.dev/doc/code#Testing
- testify API 文档: https://pkg.go.dev/github.com/stretchr/testify