# Golang Ut

> 使用 testify 库为 Go 代码生成和维护单元测试。在每次代码生成或修改后，自动为重要逻辑生成或更新单元测试，确保代码质量和测试覆盖率。当用户需要编写单元测试、提高测试覆盖率，或确保代码逻辑正确性时使用此 skill。

- Skill: `migoxlab/golang-ut` (Agent Skill)
- Install (CLI): `npx skillmds@latest add migoxlab/golang-ut`
- Raw SKILL.md: https://api.skillmd.com/api/skills/migoxlab/golang-ut/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: migoxlab (https://skillmd.com/u/migoxlab)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/migoxlab/golang-ut

---


# Go 单元测试 Skill

## 描述

这个 skill 帮助开发者使用 [testify](https://github.com/stretchr/testify) 库为 Go 代码生成和维护单元测试。当 AI 生成或修改 Go 代码后，会自动为重要逻辑生成或更新单元测试，确保代码质量和测试覆盖率。

## 何时使用

在以下场景中使用这个 skill：

- AI 生成或修改了 Go 代码后，需要为重要逻辑生成单元测试
- 用户需要为现有代码编写单元测试
- 用户需要提高代码的测试覆盖率
- 用户需要确保代码逻辑的正确性
- 用户询问如何使用 testify 编写单元测试
- 用户需要更新现有测试用例以匹配代码变更

## 前置要求

在使用 testify 之前，确保满足以下依赖：

1. **Golang**: >= 1.19
2. **testify**: 最新版本（通过 `go get` 安装）
3. **测试环境**: 能够运行 `go test` 命令

## 安装 testify

### 安装步骤

在项目中使用 testify：

```bash
go get github.com/stretchr/testify
```

### 验证安装

```bash
go list -m github.com/stretchr/testify
```

应该显示 testify 的版本信息。

## AI 交互指导

### ⚠️ 重要：自动生成和维护单元测试

**关键原则：在每次生成或修改 Go 代码后，AI 必须：**

1. **识别重要逻辑** - 分析代码，识别需要测试的重要函数、方法和业务逻辑
2. **检查现有测试** - 检查是否已存在对应的测试文件
3. **生成或更新测试** - 为重要逻辑生成新的测试用例或更新现有测试
4. **使用 testify** - 使用 testify 的 `assert` 和 `require` 包编写测试
5. **运行测试** - 执行 `go test` 验证测试通过
6. **确保覆盖率** - 确保关键逻辑有足够的测试覆盖

**禁止行为：**

- ❌ 禁止跳过单元测试生成
- ❌ 禁止忽略测试失败
- ❌ 禁止在修改代码后不更新相关测试
- ❌ 禁止只测试简单逻辑而忽略复杂业务逻辑

### 1. 标准工作流程

**当 AI 生成或修改 Go 代码后，必须遵循以下流程：**

1. **分析代码** - 识别需要测试的重要函数和方法
2. **检查测试文件** - 检查是否存在对应的 `*_test.go` 文件
3. **生成测试代码** - 使用 testify 生成测试用例
4. **运行测试** - 执行 `go test -v ./...` 或针对特定包的测试
5. **修复测试** - 如果测试失败，修复测试代码或业务代码
6. **验证覆盖率** - 运行 `go test -cover` 检查测试覆盖率
7. **报告结果** - 向用户报告测试生成和运行的结果

### 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` 包提供断言方法，失败时不会终止测试：

```go
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 实例：

```go
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` 相同的函数，但失败时会立即终止测试：

```go
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 对象，用于测试依赖外部服务的代码：

```go
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：

```go
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）：

```go
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. 测试场景覆盖

每个函数应该测试以下场景：

- **正常场景** - 正常输入和预期输出
- **边界场景** - 边界值、空值、零值
- **异常场景** - 错误输入、异常情况
- **错误处理** - 错误返回和错误信息

示例：

```go
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. 使用表驱动测试

对于多个相似场景，使用表驱动测试：

```go
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 对象隔离外部依赖：

```go
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)
	})
}
```

## 运行测试

### 基本测试命令

```bash
# 运行当前包的所有测试
go test

# 运行测试并显示详细信息
go test -v

# 运行特定测试函数
go test -v -run TestFunctionName

# 运行测试并显示覆盖率
go test -cover

# 运行测试并生成覆盖率报告
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
```

### 运行特定包的测试

```bash
# 运行特定包的测试
go test ./internal/service

# 运行所有包的测试
go test ./...

# 运行所有包的测试并显示覆盖率
go test -cover ./...
```

### 测试覆盖率

```bash
# 查看覆盖率
go test -cover ./...

# 生成详细的覆盖率报告
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out

# 生成 HTML 覆盖率报告
go tool cover -html=coverage.out
```

## 常用测试场景

### 1. 测试 HTTP Handler

```go
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. 测试数据库操作

```go
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. 测试并发安全

```go
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 配置文件。仅当用户要求时，才参考以下配置：

```yaml
# .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 中添加测试命令：

```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
```

## 工作流程示例

### 典型的新功能开发流程

1. **AI 生成业务代码** - 根据用户需求生成新的 Go 代码
2. **识别重要逻辑** - AI 分析代码，识别需要测试的函数和方法
3. **生成测试代码** - AI 使用 testify 生成单元测试
4. **运行测试** - AI 执行 `go test -v` 验证测试
5. **修复问题** - 如果测试失败，修复测试或业务代码
6. **验证覆盖率** - AI 运行 `go test -cover` 检查覆盖率
7. **提交代码** - 用户确认后提交代码和测试

### 代码修改流程

1. **AI 修改业务代码** - 根据用户需求修改现有代码
2. **检查相关测试** - AI 检查是否存在对应的测试文件
3. **更新测试用例** - AI 更新测试以匹配代码变更
4. **运行测试验证** - AI 执行测试确保所有测试通过
5. **添加新场景测试** - 如果代码变更引入了新场景，添加相应测试

## 故障排查

### 问题：testify 未安装

**症状**：导入 testify 时出现错误

**解决方案**：

```bash
go get github.com/stretchr/testify
```

### 问题：测试失败

**症状**：运行测试时出现失败

**解决方案**：

1. 仔细阅读错误信息
2. 检查测试断言是否正确
3. 检查 mock 对象的期望设置
4. 验证测试数据是否正确

### 问题：测试覆盖率低

**症状**：覆盖率报告显示覆盖率较低

**解决方案**：

1. 识别未覆盖的代码路径
2. 为重要逻辑添加测试用例
3. 使用 `go tool cover -html=coverage.out` 查看详细覆盖情况
4. 优先覆盖业务核心逻辑

### 问题：Mock 对象未按预期工作

**症状**：Mock 对象的期望未被满足

**解决方案**：

1. 检查 `On()` 方法的参数是否匹配实际调用
2. 使用 `mock.Anything` 或 `mock.MatchedBy()` 处理复杂参数
3. 确保调用了 `AssertExpectations(t)`
4. 检查 mock 对象是否正确传递到被测试代码

## 最佳实践

1. **测试命名清晰** - 测试函数名应该清楚描述测试的场景
2. **一个测试一个场景** - 每个测试函数只测试一个场景
3. **使用表驱动测试** - 对于多个相似场景，使用表驱动测试
4. **隔离外部依赖** - 使用 mock 对象隔离外部依赖
5. **测试边界条件** - 确保测试边界值和异常情况
6. **保持测试简单** - 测试代码应该简单易懂
7. **及时更新测试** - 修改代码后及时更新相关测试
8. **关注覆盖率** - 确保重要逻辑有足够的测试覆盖

## 相关资源

- testify 官方文档: [https://github.com/stretchr/testify](https://github.com/stretchr/testify)
- Go 测试文档: [https://go.dev/doc/code#Testing](https://go.dev/doc/code#Testing)
- testify API 文档: [https://pkg.go.dev/github.com/stretchr/testify](https://pkg.go.dev/github.com/stretchr/testify)
