1 介绍
1.1 定义
01.技术概述
a.基本概念
testify是Go语言生态系统中最流行的测试工具库,由Stretchr团队开发和维护。它提供了一套完整的测试工具集,包括断言assertion、模拟mock和测试套件suite等功能,旨在简化Go语言的测试代码编写,提高测试代码的可读性和可维护性。testify已经成为Go社区事实上的测试标准库扩展。
b.核心定位
testify定位为Go标准testing包的增强工具库,而非替代品。它在保持Go语言简洁哲学的基础上,提供了更丰富的断言方法、强大的mock功能和灵活的测试套件组织能力。testify与标准库完全兼容,可以无缝集成到现有的测试框架中。
c.发展历程
testify于2012年首次发布,经过十多年的发展,已经成为GitHub上Star数量最多的Go测试库之一,拥有超过2万个Star。项目活跃度高,社区贡献者众多,版本迭代稳定,是Go语言测试领域最成熟的解决方案之一。
02.设计理念
a.简洁优雅
a.链式断言
testify提供流畅的链式断言API,让测试代码读起来更像自然语言,显著提高了代码的可读性。每个断言方法都返回布尔值,支持灵活的错误处理。
b.代码示例
---
// testify基础示例:简单的断言测试
package example
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Add(a, b int) int {
return a + b
}
// 测试函数:使用testify的assert包
func TestAdd(t *testing.T) {
// 创建断言对象
assert := assert.New(t)
// 基础相等断言
result := Add(2, 3)
assert.Equal(5, result, "2 + 3 应该等于 5")
// 不等断言
assert.NotEqual(6, result, "结果不应该等于 6")
// 大于断言
assert.Greater(result, 4, "结果应该大于 4")
// 零值断言
assert.NotZero(result, "结果不应该是零值")
}
// 测试函数:使用require包(断言失败立即终止)
func TestAddWithRequire(t *testing.T) {
require := require.New(t)
result := Add(2, 3)
// require断言失败会立即终止测试
require.Equal(5, result, "2 + 3 必须等于 5")
// 如果上面的断言失败,这行代码不会执行
require.Positive(result, "结果必须是正数")
}
---
b.功能完整
a.全面的断言方法
testify提供超过100个断言方法,覆盖相等性判断、类型检查、集合操作、错误处理、panic捕获等各种测试场景,几乎涵盖了所有常见的测试需求。
b.强大的Mock能力
内置完整的mock框架,支持接口mock、方法期望设置、参数匹配、返回值控制和调用验证。相比手写mock对象,testify的mock功能大大简化了依赖隔离的测试代码编写。
c.灵活的测试套件
提供suite包实现测试套件功能,支持SetUp和TearDown钩子函数、测试夹具管理、子测试组织等高级特性,帮助开发者构建结构化的测试代码。
03.核心价值
a.提升开发效率
a.减少样板代码
使用testify可以大幅减少测试代码中的样板代码。传统的Go测试需要大量的if判断和t.Errorf调用,而testify通过简洁的断言API将多行代码压缩为一行。
b.对比示例
---
// 对比:标准库 vs testify
package example
import (
"testing"
"github.com/stretchr/testify/assert"
)
// 使用标准库testing的测试(冗长)
func TestWithStandardLibrary(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
if result == 6 {
t.Errorf("Add(2, 3) should not equal 6")
}
if result <= 4 {
t.Errorf("Add(2, 3) = %d; want > 4", result)
}
}
// 使用testify的测试(简洁)
func TestWithTestify(t *testing.T) {
result := Add(2, 3)
// 三个断言,代码清晰简洁
assert.Equal(t, 5, result)
assert.NotEqual(t, 6, result)
assert.Greater(t, result, 4)
}
// 更复杂的场景:切片比较
func TestSliceComparison(t *testing.T) {
expected := []int{1, 2, 3}
actual := []int{1, 2, 3}
// 标准库需要手动遍历比较
if len(expected) != len(actual) {
t.Errorf("length mismatch")
}
for i := range expected {
if expected[i] != actual[i] {
t.Errorf("element %d: got %d, want %d", i, actual[i], expected[i])
}
}
// testify一行搞定
assert.Equal(t, expected, actual)
}
---
b.提高代码质量
a.清晰的错误信息
testify在断言失败时提供详细的错误信息,包括期望值、实际值和差异对比,帮助开发者快速定位问题。支持自定义错误消息,进一步增强调试体验。
b.强制最佳实践
通过require包强制关键断言必须通过,避免测试在错误状态下继续执行。通过mock验证强制检查依赖调用,确保测试覆盖了所有关键路径。
c.促进测试文化
降低测试代码编写门槛,让开发者更愿意编写测试。提供统一的测试代码风格,提高团队协作效率。丰富的功能支持促进TDD开发模式的实践。
1.2 核心概念
01.断言系统
a.assert包
a.功能定位
assert包提供非致命性断言,断言失败时会记录错误信息但不会终止测试执行。适用于需要检查多个条件的场景,即使某个断言失败,后续断言仍会继续执行,帮助一次性发现多个问题。
b.使用方式
---
// assert包使用示例:非致命性断言
package example
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAssertExample(t *testing.T) {
// 方式1:每次调用都传递t参数
assert.Equal(t, 5, Add(2, 3), "基础加法测试")
assert.NotNil(t, "hello")
// 方式2:创建断言对象,避免重复传递t
a := assert.New(t)
a.Equal(5, Add(2, 3))
a.NotNil("hello")
// 即使前面的断言失败,这里仍会执行
a.True(true, "这个断言会执行")
}
// 多个断言的测试示例
func TestMultipleAssertions(t *testing.T) {
a := assert.New(t)
user := GetUser(1)
// 检查多个字段,即使某个失败也继续检查
a.NotNil(user, "用户不应为nil")
a.Equal("Alice", user.Name, "用户名应为Alice")
a.Equal(25, user.Age, "年龄应为25")
a.True(user.Active, "用户应处于激活状态")
// 所有断言都会执行,最后统一报告失败信息
}
type User struct {
Name string
Age int
Active bool
}
func GetUser(id int) *User {
return &User{Name: "Alice", Age: 25, Active: true}
}
---
b.require包
a.功能定位
require包提供致命性断言,断言失败时会立即终止当前测试函数的执行。适用于前置条件检查,如果前置条件不满足,继续执行测试没有意义且可能导致panic。
b.使用场景
---
// require包使用示例:致命性断言
package example
import (
"testing"
"github.com/stretchr/testify/require"
"database/sql"
)
func TestRequireExample(t *testing.T) {
r := require.New(t)
// 数据库连接必须成功,否则后续测试无法进行
db, err := sql.Open("mysql", "connection_string")
r.NoError(err, "数据库连接必须成功")
r.NotNil(db, "数据库对象不能为nil")
// 如果上面的require失败,这里的代码不会执行
defer db.Close()
// 查询必须成功
rows, err := db.Query("SELECT * FROM users")
r.NoError(err)
defer rows.Close()
// 至少要有一条记录
r.True(rows.Next(), "至少应该有一条用户记录")
}
// require vs assert 选择示例
func TestRequireVsAssert(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// 使用require检查前置条件
config := LoadConfig()
r.NotNil(config, "配置文件必须加载成功")
// 使用assert检查多个配置项
a.Equal("localhost", config.Host)
a.Equal(3306, config.Port)
a.Equal("utf8mb4", config.Charset)
// 即使某个配置项不对,也能看到所有配置的实际值
}
type Config struct {
Host string
Port int
Charset string
}
func LoadConfig() *Config {
return &Config{Host: "localhost", Port: 3306, Charset: "utf8mb4"}
}
---
02.Mock系统
a.Mock对象
a.基本原理
testify的mock包提供了一套完整的模拟对象框架,通过继承mock.Mock结构体并实现接口方法,可以轻松创建mock对象。mock对象可以设置方法调用期望、模拟返回值、验证调用次数和参数,实现完全的依赖隔离。
b.工作流程
Mock测试的典型流程包括三个步骤:定义期望On、执行被测代码、验证调用AssertExpectations。这种结构化的流程确保测试的完整性和可靠性。
c.代码示例
---
// Mock对象使用示例
package example
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)
// 定义接口
type UserRepository interface {
GetUserByID(id int) (*User, error)
SaveUser(user *User) error
}
// 创建Mock对象(继承mock.Mock)
type MockUserRepository struct {
mock.Mock
}
// 实现接口方法
func (m *MockUserRepository) GetUserByID(id int) (*User, error) {
// 调用mock框架记录调用并返回预设值
args := m.Called(id)
return args.Get(0).(*User), args.Error(1)
}
func (m *MockUserRepository) SaveUser(user *User) error {
args := m.Called(user)
return args.Error(0)
}
// 被测试的服务
type UserService struct {
repo UserRepository
}
func (s *UserService) GetUserName(id int) (string, error) {
user, err := s.repo.GetUserByID(id)
if err != nil {
return "", err
}
return user.Name, nil
}
// Mock测试示例
func TestUserService_GetUserName(t *testing.T) {
// 1. 创建mock对象
mockRepo := new(MockUserRepository)
// 2. 设置期望:当调用GetUserByID(1)时返回指定用户
expectedUser := &User{Name: "Alice", Age: 25}
mockRepo.On("GetUserByID", 1).Return(expectedUser, nil)
// 3. 创建被测服务,注入mock对象
service := &UserService{repo: mockRepo}
// 4. 执行测试
name, err := service.GetUserName(1)
// 5. 断言结果
assert.NoError(t, err)
assert.Equal(t, "Alice", name)
// 6. 验证mock对象的方法被正确调用
mockRepo.AssertExpectations(t)
}
---
b.参数匹配器
a.功能说明
testify提供多种参数匹配器,包括精确匹配、类型匹配mock.Anything、自定义匹配mock.MatchedBy等。参数匹配器增强了mock的灵活性,可以处理复杂的参数验证场景。
b.使用示例
---
// 参数匹配器示例
package example
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)
func TestParameterMatchers(t *testing.T) {
mockRepo := new(MockUserRepository)
// 精确匹配:只匹配特定参数
mockRepo.On("GetUserByID", 1).Return(&User{Name: "Alice"}, nil)
// Anything匹配:匹配任意参数
mockRepo.On("GetUserByID", mock.Anything).Return(&User{Name: "Default"}, nil)
// AnythingOfType匹配:匹配特定类型
mockRepo.On("SaveUser", mock.AnythingOfType("*example.User")).Return(nil)
// MatchedBy自定义匹配:使用函数进行复杂匹配
mockRepo.On("GetUserByID", mock.MatchedBy(func(id int) bool {
return id > 0 && id < 1000
})).Return(&User{Name: "ValidID"}, nil)
// 测试执行
user1, _ := mockRepo.GetUserByID(1)
assert.Equal(t, "Alice", user1.Name)
user2, _ := mockRepo.GetUserByID(999)
assert.Equal(t, "ValidID", user2.Name)
err := mockRepo.SaveUser(&User{Name: "Bob"})
assert.NoError(t, err)
mockRepo.AssertExpectations(t)
}
---
03.测试套件
a.Suite结构
a.基本概念
testify的suite包提供测试套件功能,通过将测试方法组织到结构体中,可以实现测试的结构化管理。Suite支持SetUp和TearDown钩子函数,方便管理测试的前置和后置操作。
b.核心方法
Suite提供多个生命周期钩子:SetupSuite在整个套件开始前执行一次,TearDownSuite在套件结束后执行一次,SetupTest在每个测试方法前执行,TearDownTest在每个测试方法后执行。这种分层的生命周期管理使测试更加规范和可控。
c.代码示例
---
// 测试套件示例
package example
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/stretchr/testify/assert"
)
// 定义测试套件结构体
type UserTestSuite struct {
suite.Suite
repo UserRepository
service *UserService
}
// SetupSuite:在整个套件开始前执行一次
func (s *UserTestSuite) SetupSuite() {
// 初始化数据库连接等全局资源
s.repo = NewUserRepository()
}
// TearDownSuite:在套件结束后执行一次
func (s *UserTestSuite) TearDownSuite() {
// 清理全局资源
s.repo.Close()
}
// SetupTest:在每个测试方法前执行
func (s *UserTestSuite) SetupTest() {
// 初始化测试数据
s.service = &UserService{repo: s.repo}
}
// TearDownTest:在每个测试方法后执行
func (s *UserTestSuite) TearDownTest() {
// 清理测试数据
s.repo.CleanTestData()
}
// 测试方法:必须以Test开头
func (s *UserTestSuite) TestGetUserName() {
name, err := s.service.GetUserName(1)
assert.NoError(s.T(), err)
assert.Equal(s.T(), "Alice", name)
}
func (s *UserTestSuite) TestSaveUser() {
user := &User{Name: "Bob", Age: 30}
err := s.repo.SaveUser(user)
assert.NoError(s.T(), err)
}
// 运行测试套件
func TestUserTestSuite(t *testing.T) {
suite.Run(t, new(UserTestSuite))
}
// 辅助函数(示例)
func NewUserRepository() UserRepository {
return &MockUserRepository{}
}
func (m *MockUserRepository) Close() {}
func (m *MockUserRepository) CleanTestData() {}
---
b.测试组织
Suite允许将相关测试组织在一起,共享测试夹具和辅助方法。通过套件继承可以实现测试代码的复用,适合大型项目的测试管理。
1.3 优缺点
01.优势分析
a.简化测试代码
a.减少代码量
相比使用Go标准库编写测试,testify可以将测试代码量减少50%以上。通过简洁的断言API和自动化的错误信息生成,大幅降低了测试代码的复杂度。
b.对比示例
---
// 标准库 vs testify 代码量对比
package example
import (
"testing"
"github.com/stretchr/testify/assert"
"reflect"
)
// 场景1:基础断言对比
func TestBasicStandardLib(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
if result == 0 {
t.Errorf("Add(2, 3) should not be zero")
}
if result <= 4 {
t.Errorf("Add(2, 3) = %d; should be > 4", result)
}
}
func TestBasicTestify(t *testing.T) {
result := Add(2, 3)
assert.Equal(t, 5, result)
assert.NotZero(t, result)
assert.Greater(t, result, 4)
}
// 场景2:复杂结构体对比
func TestStructStandardLib(t *testing.T) {
expected := &User{Name: "Alice", Age: 25, Active: true}
actual := GetUser(1)
if actual == nil {
t.Fatal("user should not be nil")
}
if actual.Name != expected.Name {
t.Errorf("Name = %s; want %s", actual.Name, expected.Name)
}
if actual.Age != expected.Age {
t.Errorf("Age = %d; want %d", actual.Age, expected.Age)
}
if actual.Active != expected.Active {
t.Errorf("Active = %v; want %v", actual.Active, expected.Active)
}
}
func TestStructTestify(t *testing.T) {
expected := &User{Name: "Alice", Age: 25, Active: true}
actual := GetUser(1)
assert.Equal(t, expected, actual)
}
// 场景3:切片对比
func TestSliceStandardLib(t *testing.T) {
expected := []int{1, 2, 3, 4, 5}
actual := GetNumbers()
if len(actual) != len(expected) {
t.Fatalf("length = %d; want %d", len(actual), len(expected))
}
if !reflect.DeepEqual(expected, actual) {
t.Errorf("slice = %v; want %v", actual, expected)
}
for i, v := range expected {
if actual[i] != v {
t.Errorf("element[%d] = %d; want %d", i, actual[i], v)
}
}
}
func TestSliceTestify(t *testing.T) {
expected := []int{1, 2, 3, 4, 5}
actual := GetNumbers()
assert.Equal(t, expected, actual)
assert.ElementsMatch(t, expected, actual)
}
func GetNumbers() []int {
return []int{1, 2, 3, 4, 5}
}
---
b.丰富的断言方法
a.全面覆盖
testify提供超过100个断言方法,涵盖基础比较Equal、NotEqual,类型检查IsType、Implements,集合操作Contains、ElementsMatch,错误处理Error、NoError,panic捕获Panics、NotPanics等各种场景。
b.常用断言分类
相等性断言包括Equal、NotEqual、Same、NotSame;数值断言包括Greater、GreaterOrEqual、Less、LessOrEqual、Positive、Negative;字符串断言包括Contains、NotContains、Regexp、NotRegexp;集合断言包括Len、Empty、Contains、ElementsMatch、Subset。
c.强大的Mock能力
a.完整的Mock框架
testify提供完整的mock功能,无需第三方工具即可实现接口mock。支持方法期望设置、参数匹配、返回值控制、调用次数验证、调用顺序验证等高级特性。
b.Mock示例
---
// 高级Mock功能示例
package example
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
"errors"
)
// 场景1:调用次数验证
func TestCallTimes(t *testing.T) {
mockRepo := new(MockUserRepository)
// 期望方法被调用1次
mockRepo.On("GetUserByID", 1).Return(&User{Name: "Alice"}, nil).Once()
// 期望方法被调用2次
mockRepo.On("SaveUser", mock.Anything).Return(nil).Twice()
service := &UserService{repo: mockRepo}
// 调用1次GetUserByID
service.GetUserName(1)
// 调用2次SaveUser
mockRepo.SaveUser(&User{Name: "Bob"})
mockRepo.SaveUser(&User{Name: "Charlie"})
// 验证调用次数
mockRepo.AssertExpectations(t)
mockRepo.AssertNumberOfCalls(t, "GetUserByID", 1)
mockRepo.AssertNumberOfCalls(t, "SaveUser", 2)
}
// 场景2:不同参数返回不同值
func TestDifferentReturns(t *testing.T) {
mockRepo := new(MockUserRepository)
// 不同ID返回不同用户
mockRepo.On("GetUserByID", 1).Return(&User{Name: "Alice"}, nil)
mockRepo.On("GetUserByID", 2).Return(&User{Name: "Bob"}, nil)
mockRepo.On("GetUserByID", 999).Return(nil, errors.New("not found"))
user1, err1 := mockRepo.GetUserByID(1)
assert.NoError(t, err1)
assert.Equal(t, "Alice", user1.Name)
user2, err2 := mockRepo.GetUserByID(2)
assert.NoError(t, err2)
assert.Equal(t, "Bob", user2.Name)
user3, err3 := mockRepo.GetUserByID(999)
assert.Error(t, err3)
assert.Nil(t, user3)
mockRepo.AssertExpectations(t)
}
// 场景3:使用Run自定义行为
func TestCustomBehavior(t *testing.T) {
mockRepo := new(MockUserRepository)
callCount := 0
mockRepo.On("GetUserByID", mock.Anything).Run(func(args mock.Arguments) {
callCount++
id := args.Int(0)
t.Logf("GetUserByID called with id=%d, total calls=%d", id, callCount)
}).Return(&User{Name: "Dynamic"}, nil)
mockRepo.GetUserByID(1)
mockRepo.GetUserByID(2)
mockRepo.GetUserByID(3)
assert.Equal(t, 3, callCount)
mockRepo.AssertExpectations(t)
}
// 场景4:测试超时场景
func TestTimeout(t *testing.T) {
mockRepo := new(MockUserRepository)
// 模拟超时错误
mockRepo.On("GetUserByID", mock.Anything).
WaitUntil(time.After(100 * time.Millisecond)).
Return(nil, errors.New("timeout"))
start := time.Now()
_, err := mockRepo.GetUserByID(1)
duration := time.Since(start)
assert.Error(t, err)
assert.GreaterOrEqual(t, duration, 100*time.Millisecond)
mockRepo.AssertExpectations(t)
}
---
d.良好的测试组织
suite包提供结构化的测试组织能力,支持SetUp/TearDown钩子、测试夹具管理、子测试分组等功能。相比零散的测试函数,suite让测试代码更加模块化和可维护。
02.局限性分析
a.学习成本
a.API数量庞大
testify提供超过100个断言方法和多个包assert、require、mock、suite、http,初学者需要时间熟悉各个API的用途和差异。不过常用的断言方法只有20个左右,掌握核心API即可应对大部分场景。
b.Mock理解门槛
Mock概念对测试新手来说有一定理解难度,需要理解测试替身、依赖注入、期望设置等概念。建议从简单的断言开始,逐步学习mock和suite高级功能。
b.性能开销
a.反射使用
testify的断言实现大量使用反射进行深度比较和类型检查,在高频调用场景下可能带来性能开销。不过对于绝大多数测试场景,这点性能开销可以忽略不计。
b.基准测试对比
---
// 性能对比基准测试
package example
import (
"testing"
"github.com/stretchr/testify/assert"
)
// 标准库断言性能
func BenchmarkStandardLibAssertion(b *testing.B) {
for i := 0; i < b.N; i++ {
result := Add(2, 3)
if result != 5 {
b.Errorf("unexpected result")
}
}
}
// testify断言性能
func BenchmarkTestifyAssertion(b *testing.B) {
for i := 0; i < b.N; i++ {
result := Add(2, 3)
assert.Equal(b, 5, result)
}
}
// 复杂对象深度比较性能
func BenchmarkDeepEqual(b *testing.B) {
user1 := &User{Name: "Alice", Age: 25, Active: true}
user2 := &User{Name: "Alice", Age: 25, Active: true}
b.Run("StandardLib", func(b *testing.B) {
for i := 0; i < b.N; i++ {
if user1.Name != user2.Name ||
user1.Age != user2.Age ||
user1.Active != user2.Active {
b.Errorf("not equal")
}
}
})
b.Run("Testify", func(b *testing.B) {
for i := 0; i < b.N; i++ {
assert.Equal(b, user1, user2)
}
})
}
// 结果:testify比标准库慢约2-3倍,但绝对时间仍在纳秒级
---
c.依赖管理
testify作为第三方库需要通过go mod管理依赖,增加了项目的外部依赖。不过testify本身非常稳定,版本兼容性好,升级风险低。
d.过度使用风险
a.断言滥用
过度使用断言可能导致测试代码臃肿,一个测试函数包含几十个断言会降低可读性。建议遵循单一职责原则,每个测试函数只测试一个行为。
b.Mock滥用
过度使用mock可能导致测试与实现耦合过紧,实现细节的改变会导致大量测试失败。应该只mock外部依赖接口、数据库、网络请求等,而不是mock内部逻辑。
03.适用场景评估
a.最佳适用场景
a.单元测试
testify最适合编写单元测试,丰富的断言方法和强大的mock能力能够快速验证函数行为和隔离依赖。特别适合测试业务逻辑层、服务层代码。
b.API测试
testify的http包提供HTTP测试工具,配合httptest包可以轻松测试HTTP处理器和RESTful API。适合测试Web应用、微服务接口。
c.集成测试
suite包的SetUp/TearDown机制非常适合管理集成测试的环境准备和清理。可以在SetupSuite中初始化数据库、启动服务,在TearDownSuite中清理资源。
d.TDD开发
testify简洁的API降低了编写测试的门槛,鼓励开发者采用TDD测试驱动开发模式。先写测试再写实现,提高代码质量。
b.不太适合的场景
a.端到端测试
对于需要浏览器自动化的端到端测试,testify不是最佳选择。这类测试应该使用Selenium、Cypress等专业工具。
b.性能测试
虽然testify可以配合Go的基准测试使用,但对于复杂的性能测试和压力测试,建议使用专业工具如wrk、ab、JMeter等。
c.极简项目
对于非常小型的项目或脚本,引入testify可能过于重量级。如果只需要几个简单的相等性检查,使用标准库testing包更简单。
1.4 使用场景
01.单元测试场景
a.业务逻辑测试
a.场景描述
业务逻辑层是应用的核心,包含各种业务规则、数据验证、计算逻辑等。testify的断言能力和mock功能可以快速验证业务逻辑的正确性,同时隔离外部依赖如数据库、缓存、第三方API等。
b.实战示例
---
// 业务逻辑测试示例:订单处理服务
package business
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"errors"
)
// 订单服务接口
type OrderService struct {
orderRepo OrderRepository
paymentRepo PaymentRepository
notification NotificationService
}
// 创建订单业务逻辑
func (s *OrderService) CreateOrder(userID int, items []OrderItem, amount float64) (*Order, error) {
// 1. 验证金额
if amount <= 0 {
return nil, errors.New("订单金额必须大于0")
}
// 2. 验证商品列表
if len(items) == 0 {
return nil, errors.New("订单至少包含一个商品")
}
// 3. 创建订单
order := &Order{
UserID: userID,
Items: items,
Amount: amount,
Status: "pending",
}
// 4. 保存订单
err := s.orderRepo.Save(order)
if err != nil {
return nil, err
}
// 5. 发送通知
s.notification.Send(userID, "订单创建成功")
return order, nil
}
// Mock对象定义
type MockOrderRepository struct {
mock.Mock
}
func (m *MockOrderRepository) Save(order *Order) error {
args := m.Called(order)
return args.Error(0)
}
type MockPaymentRepository struct {
mock.Mock
}
type MockNotificationService struct {
mock.Mock
}
func (m *MockNotificationService) Send(userID int, message string) error {
args := m.Called(userID, message)
return args.Error(0)
}
// 测试用例:成功创建订单
func TestCreateOrder_Success(t *testing.T) {
// 准备mock对象
mockOrderRepo := new(MockOrderRepository)
mockNotification := new(MockNotificationService)
// 设置期望
mockOrderRepo.On("Save", mock.AnythingOfType("*business.Order")).Return(nil)
mockNotification.On("Send", 1, "订单创建成功").Return(nil)
// 创建服务
service := &OrderService{
orderRepo: mockOrderRepo,
notification: mockNotification,
}
// 执行测试
items := []OrderItem{{ProductID: 1, Quantity: 2, Price: 50.0}}
order, err := service.CreateOrder(1, items, 100.0)
// 断言结果
assert.NoError(t, err)
assert.NotNil(t, order)
assert.Equal(t, 1, order.UserID)
assert.Equal(t, 100.0, order.Amount)
assert.Equal(t, "pending", order.Status)
assert.Len(t, order.Items, 1)
// 验证mock调用
mockOrderRepo.AssertExpectations(t)
mockNotification.AssertExpectations(t)
}
// 测试用例:金额验证失败
func TestCreateOrder_InvalidAmount(t *testing.T) {
service := &OrderService{}
items := []OrderItem{{ProductID: 1, Quantity: 1, Price: 50.0}}
// 测试零金额
order, err := service.CreateOrder(1, items, 0)
assert.Error(t, err)
assert.Nil(t, order)
assert.Equal(t, "订单金额必须大于0", err.Error())
// 测试负金额
order, err = service.CreateOrder(1, items, -100)
assert.Error(t, err)
assert.Nil(t, order)
}
// 测试用例:空商品列表
func TestCreateOrder_EmptyItems(t *testing.T) {
service := &OrderService{}
order, err := service.CreateOrder(1, []OrderItem{}, 100.0)
assert.Error(t, err)
assert.Nil(t, order)
assert.Contains(t, err.Error(), "至少包含一个商品")
}
// 测试用例:数据库保存失败
func TestCreateOrder_SaveFailure(t *testing.T) {
mockOrderRepo := new(MockOrderRepository)
mockOrderRepo.On("Save", mock.Anything).Return(errors.New("数据库错误"))
service := &OrderService{orderRepo: mockOrderRepo}
items := []OrderItem{{ProductID: 1, Quantity: 1, Price: 50.0}}
order, err := service.CreateOrder(1, items, 50.0)
assert.Error(t, err)
assert.Nil(t, order)
assert.Equal(t, "数据库错误", err.Error())
mockOrderRepo.AssertExpectations(t)
}
// 数据结构定义
type Order struct {
ID int
UserID int
Items []OrderItem
Amount float64
Status string
}
type OrderItem struct {
ProductID int
Quantity int
Price float64
}
type OrderRepository interface {
Save(order *Order) error
}
type PaymentRepository interface{}
type NotificationService interface {
Send(userID int, message string) error
}
---
b.数据转换测试
a.场景描述
数据转换逻辑包括DTO转换、序列化反序列化、数据格式化等。testify的Equal、JSONEq等断言方法可以轻松验证转换结果的正确性。
b.实战示例
---
// 数据转换测试示例
package converter
import (
"testing"
"github.com/stretchr/testify/assert"
"encoding/json"
"time"
)
// DTO转换器
type UserConverter struct{}
func (c *UserConverter) ToDTO(user *User) *UserDTO {
if user == nil {
return nil
}
return &UserDTO{
ID: user.ID,
Username: user.Username,
Email: user.Email,
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
IsActive: user.Active,
}
}
func (c *UserConverter) ToEntity(dto *UserDTO) (*User, error) {
if dto == nil {
return nil, errors.New("dto不能为空")
}
createdAt, err := time.Parse("2006-01-02 15:04:05", dto.CreatedAt)
if err != nil {
return nil, err
}
return &User{
ID: dto.ID,
Username: dto.Username,
Email: dto.Email,
CreatedAt: createdAt,
Active: dto.IsActive,
}, nil
}
// 测试DTO转换
func TestToDTO(t *testing.T) {
converter := &UserConverter{}
// 测试正常转换
createdAt := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
user := &User{
ID: 1,
Username: "alice",
Email: "[email protected]",
CreatedAt: createdAt,
Active: true,
}
dto := converter.ToDTO(user)
assert.NotNil(t, dto)
assert.Equal(t, 1, dto.ID)
assert.Equal(t, "alice", dto.Username)
assert.Equal(t, "[email protected]", dto.Email)
assert.Equal(t, "2024-01-01 12:00:00", dto.CreatedAt)
assert.True(t, dto.IsActive)
// 测试nil处理
nilDTO := converter.ToDTO(nil)
assert.Nil(t, nilDTO)
}
// 测试Entity转换
func TestToEntity(t *testing.T) {
converter := &UserConverter{}
dto := &UserDTO{
ID: 1,
Username: "bob",
Email: "[email protected]",
CreatedAt: "2024-01-01 12:00:00",
IsActive: true,
}
user, err := converter.ToEntity(dto)
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, 1, user.ID)
assert.Equal(t, "bob", user.Username)
assert.Equal(t, "[email protected]", user.Email)
assert.True(t, user.Active)
assert.Equal(t, 2024, user.CreatedAt.Year())
// 测试nil处理
nilUser, err := converter.ToEntity(nil)
assert.Error(t, err)
assert.Nil(t, nilUser)
// 测试无效日期
invalidDTO := &UserDTO{CreatedAt: "invalid-date"}
invalidUser, err := converter.ToEntity(invalidDTO)
assert.Error(t, err)
assert.Nil(t, invalidUser)
}
// 测试JSON序列化
func TestJSONSerialization(t *testing.T) {
user := &User{
ID: 1,
Username: "charlie",
Email: "[email protected]",
Active: true,
}
// 序列化
jsonData, err := json.Marshal(user)
assert.NoError(t, err)
// 使用JSONEq断言JSON相等(忽略字段顺序和格式)
expectedJSON := `{
"id": 1,
"username": "charlie",
"email": "[email protected]",
"active": true
}`
assert.JSONEq(t, expectedJSON, string(jsonData))
// 反序列化
var decoded User
err = json.Unmarshal(jsonData, &decoded)
assert.NoError(t, err)
assert.Equal(t, user.ID, decoded.ID)
assert.Equal(t, user.Username, decoded.Username)
}
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
Active bool `json:"active"`
}
type UserDTO struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
CreatedAt string `json:"created_at"`
IsActive bool `json:"is_active"`
}
---
02.集成测试场景
a.数据库集成测试
a.场景描述
测试应用与数据库的集成,验证SQL查询、事务处理、数据一致性等。suite包的SetUp/TearDown机制非常适合管理测试数据库的初始化和清理。
b.实战示例
---
// 数据库集成测试示例
package repository
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/stretchr/testify/assert"
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
// 数据库测试套件
type UserRepositoryTestSuite struct {
suite.Suite
db *sql.DB
repo *UserRepository
}
// 套件初始化:连接测试数据库
func (s *UserRepositoryTestSuite) SetupSuite() {
var err error
s.db, err = sql.Open("mysql", "root:password@tcp(localhost:3306)/test_db")
s.Require().NoError(err)
s.repo = NewUserRepository(s.db)
}
// 套件清理:关闭数据库连接
func (s *UserRepositoryTestSuite) TearDownSuite() {
if s.db != nil {
s.db.Close()
}
}
// 每个测试前:清空表并插入测试数据
func (s *UserRepositoryTestSuite) SetupTest() {
_, err := s.db.Exec("TRUNCATE TABLE users")
s.Require().NoError(err)
// 插入测试数据
_, err = s.db.Exec(`
INSERT INTO users (id, username, email, active) VALUES
(1, 'alice', '[email protected]', 1),
(2, 'bob', '[email protected]', 1),
(3, 'charlie', '[email protected]', 0)
`)
s.Require().NoError(err)
}
// 测试:根据ID查询用户
func (s *UserRepositoryTestSuite) TestFindByID() {
user, err := s.repo.FindByID(1)
assert.NoError(s.T(), err)
assert.NotNil(s.T(), user)
assert.Equal(s.T(), 1, user.ID)
assert.Equal(s.T(), "alice", user.Username)
assert.Equal(s.T(), "[email protected]", user.Email)
assert.True(s.T(), user.Active)
}
// 测试:查询不存在的用户
func (s *UserRepositoryTestSuite) TestFindByID_NotFound() {
user, err := s.repo.FindByID(999)
assert.Error(s.T(), err)
assert.Nil(s.T(), user)
assert.Equal(s.T(), sql.ErrNoRows, err)
}
// 测试:查询所有激活用户
func (s *UserRepositoryTestSuite) TestFindActiveUsers() {
users, err := s.repo.FindActiveUsers()
assert.NoError(s.T(), err)
assert.Len(s.T(), users, 2)
assert.Equal(s.T(), "alice", users[0].Username)
assert.Equal(s.T(), "bob", users[1].Username)
}
// 测试:创建用户
func (s *UserRepositoryTestSuite) TestCreate() {
newUser := &User{
Username: "dave",
Email: "[email protected]",
Active: true,
}
err := s.repo.Create(newUser)
assert.NoError(s.T(), err)
assert.NotZero(s.T(), newUser.ID)
// 验证数据库中确实存在
savedUser, err := s.repo.FindByID(newUser.ID)
assert.NoError(s.T(), err)
assert.Equal(s.T(), "dave", savedUser.Username)
}
// 测试:更新用户
func (s *UserRepositoryTestSuite) TestUpdate() {
user, _ := s.repo.FindByID(1)
user.Email = "[email protected]"
err := s.repo.Update(user)
assert.NoError(s.T(), err)
// 验证更新成功
updated, _ := s.repo.FindByID(1)
assert.Equal(s.T(), "[email protected]", updated.Email)
}
// 测试:删除用户
func (s *UserRepositoryTestSuite) TestDelete() {
err := s.repo.Delete(1)
assert.NoError(s.T(), err)
// 验证用户已删除
user, err := s.repo.FindByID(1)
assert.Error(s.T(), err)
assert.Nil(s.T(), user)
}
// 运行测试套件
func TestUserRepositoryTestSuite(t *testing.T) {
suite.Run(t, new(UserRepositoryTestSuite))
}
// Repository实现(简化示例)
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) FindByID(id int) (*User, error) {
user := &User{}
err := r.db.QueryRow(
"SELECT id, username, email, active FROM users WHERE id = ?",
id,
).Scan(&user.ID, &user.Username, &user.Email, &user.Active)
return user, err
}
func (r *UserRepository) FindActiveUsers() ([]*User, error) {
rows, err := r.db.Query("SELECT id, username, email, active FROM users WHERE active = 1")
if err != nil {
return nil, err
}
defer rows.Close()
var users []*User
for rows.Next() {
user := &User{}
rows.Scan(&user.ID, &user.Username, &user.Email, &user.Active)
users = append(users, user)
}
return users, nil
}
func (r *UserRepository) Create(user *User) error {
result, err := r.db.Exec(
"INSERT INTO users (username, email, active) VALUES (?, ?, ?)",
user.Username, user.Email, user.Active,
)
if err != nil {
return err
}
id, _ := result.LastInsertId()
user.ID = int(id)
return nil
}
func (r *UserRepository) Update(user *User) error {
_, err := r.db.Exec(
"UPDATE users SET username=?, email=?, active=? WHERE id=?",
user.Username, user.Email, user.Active, user.ID,
)
return err
}
func (r *UserRepository) Delete(id int) error {
_, err := r.db.Exec("DELETE FROM users WHERE id = ?", id)
return err
}
---
b.API集成测试
testify的httptest工具配合标准库可以轻松测试HTTP处理器。适合测试RESTful API、Web服务接口等,验证路由、参数解析、响应格式等。
03.TDD开发场景
a.测试驱动开发
testify简洁的API降低了编写测试的门槛,非常适合TDD开发模式。先编写失败的测试用例,明确期望行为,再实现功能使测试通过,最后重构优化代码。
b.红绿重构循环
红阶段编写失败测试,绿阶段快速实现让测试通过,重构阶段优化代码质量。testify的快速反馈帮助开发者保持高效的TDD节奏。
1.5 架构设计
01.包结构设计
a.核心包组织
a.assert包
assert包是testify的核心,提供非致命性断言功能。包含Assertions结构体作为断言方法的接收者,所有断言方法都返回布尔值表示断言是否通过。内部使用testing.T的Errorf方法记录失败信息,不会终止测试执行。
b.require包
require包提供致命性断言,与assert包API完全一致,但实现机制不同。断言失败时调用testing.T的FailNow方法立即终止测试。require包实际上是对assert包的封装,通过tHelper标记和FailNow实现致命性行为。
c.mock包
mock包实现完整的mock框架。核心是Mock结构体,维护期望列表ExpectedCalls和实际调用列表Calls。提供On方法设置期望、Called方法记录调用、AssertExpectations方法验证期望。使用mutex保证并发安全。
d.suite包
suite包提供测试套件功能。定义Suite接口和TestingSuite结构体,通过反射机制扫描测试方法、执行生命周期钩子。Run函数是套件的入口点,负责协调整个测试执行流程。
b.包依赖关系
---
// testify包依赖关系图(伪代码展示)
package architecture
/*
testify包依赖结构:
┌─────────────────────────────────────┐
│ testify/ │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ assert │ │ require │ │
│ │ (核心) │←───│ (封装) │ │
│ └──────────┘ └──────────┘ │
│ ↑ │
│ │ │
│ ┌────┴────┐ ┌──────────┐ │
│ │ mock │ │ suite │ │
│ │ (独立) │ │ (组织) │ │
│ └─────────┘ └────┬─────┘ │
│ │ │
│ ┌──────────┐ ┌──┴──────┐ │
│ │ http │ │ testing │ │
│ │ (辅助) │ │ (标准库) │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────┘
核心依赖说明:
1. assert包:核心包,依赖标准库testing和reflect
2. require包:封装assert包,添加致命性行为
3. mock包:独立包,不依赖assert/require
4. suite包:依赖testing包,通过反射执行测试
5. http包:辅助包,封装httptest功能
*/
// 示例:包引入方式
import (
"testing"
// 方式1:分别引入所需包
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
// 方式2:使用别名避免冲突
tassert "github.com/stretchr/testify/assert"
trequire "github.com/stretchr/testify/require"
)
// 包之间的协作示例
func TestPackageIntegration(t *testing.T) {
// assert和require可以混合使用
r := require.New(t)
a := assert.New(t)
// require检查前置条件
config := LoadConfig()
r.NotNil(config, "配置必须加载成功")
// assert检查多个属性
a.Equal("localhost", config.Host)
a.Equal(3306, config.Port)
// mock对象独立使用
mockRepo := new(MockRepository)
mockRepo.On("Get", 1).Return("data", nil)
// 在suite中使用assert/require/mock
// suite.Run(t, new(IntegrationTestSuite))
}
type Config struct {
Host string
Port int
}
func LoadConfig() *Config {
return &Config{Host: "localhost", Port: 3306}
}
type MockRepository struct {
mock.Mock
}
func (m *MockRepository) Get(id int) (string, error) {
args := m.Called(id)
return args.String(0), args.Error(1)
}
---
02.断言实现机制
a.断言流程
a.调用流程
断言方法的执行流程包括:参数接收、类型检查、值比较、结果判断、错误记录。所有断言方法都遵循相同的模式,确保行为一致性和可预测性。
b.错误记录机制
断言失败时,testify会生成详细的错误信息,包括文件位置、行号、期望值、实际值、差异说明。使用testing.T的Helper标记确保错误堆栈指向正确的测试代码行,而不是testify内部实现。
c.实现示例
---
// 断言实现机制示例(简化版)
package assert
import (
"testing"
"fmt"
"reflect"
)
// Assertions结构体:断言方法的接收者
type Assertions struct {
t TestingT
}
// TestingT接口:抽象testing.T
type TestingT interface {
Errorf(format string, args ...interface{})
Helper()
}
// New:创建断言对象
func New(t TestingT) *Assertions {
return &Assertions{t: t}
}
// Equal断言实现(简化版)
func (a *Assertions) Equal(expected, actual interface{}, msgAndArgs ...interface{}) bool {
// 标记为辅助函数,错误堆栈跳过此层
a.t.Helper()
// 使用reflect.DeepEqual进行深度比较
if reflect.DeepEqual(expected, actual) {
return true
}
// 断言失败,生成错误信息
message := formatMessage(msgAndArgs...)
diff := generateDiff(expected, actual)
a.t.Errorf("Not equal: \n"+
"expected: %v\n"+
"actual : %v\n"+
"%s\n"+
"%s",
expected, actual, diff, message)
return false
}
// NoError断言实现(简化版)
func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) bool {
a.t.Helper()
if err == nil {
return true
}
message := formatMessage(msgAndArgs...)
a.t.Errorf("Received unexpected error:\n"+
"%+v\n"+
"%s",
err, message)
return false
}
// True断言实现(简化版)
func (a *Assertions) True(value bool, msgAndArgs ...interface{}) bool {
a.t.Helper()
if value {
return true
}
message := formatMessage(msgAndArgs...)
a.t.Errorf("Should be true\n%s", message)
return false
}
// 辅助函数:格式化用户消息
func formatMessage(msgAndArgs ...interface{}) string {
if len(msgAndArgs) == 0 {
return ""
}
if len(msgAndArgs) == 1 {
return fmt.Sprintf("%v", msgAndArgs[0])
}
return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...)
}
// 辅助函数:生成差异说明
func generateDiff(expected, actual interface{}) string {
// 实际实现会生成详细的diff信息
return fmt.Sprintf("Diff: expected %T but got %T", expected, actual)
}
// 使用示例
func TestAssertionMechanism(t *testing.T) {
a := New(t)
// Equal断言
a.Equal(5, Add(2, 3), "加法结果应该正确")
// NoError断言
err := DoSomething()
a.NoError(err, "不应该返回错误")
// True断言
a.True(IsValid(), "应该返回true")
}
func Add(a, b int) int { return a + b }
func DoSomething() error { return nil }
func IsValid() bool { return true }
---
b.require vs assert区别
a.实现差异
require包通过在assert包基础上添加FailNow调用实现致命性。关键代码是在断言失败后调用t.FailNow(),该方法会立即终止当前goroutine的执行。
b.对比实现
---
// require vs assert实现对比
package comparison
import "testing"
// assert包实现(简化)
type AssertAssertions struct {
t *testing.T
}
func (a *AssertAssertions) Equal(expected, actual interface{}) bool {
a.t.Helper()
if expected != actual {
a.t.Errorf("Not equal: expected=%v, actual=%v", expected, actual)
return false // 返回false但继续执行
}
return true
}
// require包实现(简化)
type RequireAssertions struct {
t *testing.T
}
func (r *RequireAssertions) Equal(expected, actual interface{}) {
r.t.Helper()
if expected != actual {
r.t.Errorf("Not equal: expected=%v, actual=%v", expected, actual)
r.t.FailNow() // 关键差异:立即终止
}
}
// 行为对比测试
func TestAssertBehavior(t *testing.T) {
a := &AssertAssertions{t: t}
a.Equal(1, 2) // 失败但继续
t.Log("这行会执行") // 会执行
a.Equal(3, 3) // 继续执行后续断言
}
func TestRequireBehavior(t *testing.T) {
r := &RequireAssertions{t: t}
r.Equal(1, 2) // 失败并终止
t.Log("这行不会执行") // 不会执行
}
---
03.Mock框架架构
a.核心数据结构
a.Mock结构体
Mock结构体是mock框架的核心,包含ExpectedCalls期望调用列表、Calls实际调用列表、mutex并发锁、testData测试数据。所有mock对象都嵌入Mock结构体,继承其方法和状态。
b.Call结构体
Call结构体表示一次方法调用期望,包含Method方法名、Arguments参数列表、ReturnArguments返回值、Times调用次数限制、WaitFor等待时间、Run自定义函数等属性。
c.架构示例
---
// Mock框架核心结构(简化版)
package mock
import (
"sync"
"time"
"fmt"
)
// Mock核心结构体
type Mock struct {
// 期望调用列表
ExpectedCalls []*Call
// 实际调用列表
Calls []Call
// 并发锁
mutex sync.Mutex
// 测试对象
testData TestingT
}
// Call表示一次方法调用期望
type Call struct {
Method string // 方法名
Arguments []interface{} // 期望参数
ReturnArguments []interface{} // 返回值
Times int // 调用次数限制
WaitFor time.Duration // 等待时间
RunFn func(Arguments) // 自定义执行函数
}
// On:设置方法期望
func (m *Mock) On(methodName string, arguments ...interface{}) *Call {
m.mutex.Lock()
defer m.mutex.Unlock()
call := &Call{
Method: methodName,
Arguments: arguments,
}
m.ExpectedCalls = append(m.ExpectedCalls, call)
return call
}
// Return:设置返回值
func (c *Call) Return(returnArguments ...interface{}) *Call {
c.ReturnArguments = returnArguments
return c
}
// Once:设置调用一次
func (c *Call) Once() *Call {
return c.Times(1)
}
// Times:设置调用次数
func (c *Call) Times(i int) *Call {
c.Times = i
return c
}
// Called:记录实际调用
func (m *Mock) Called(arguments ...interface{}) Arguments {
m.mutex.Lock()
defer m.mutex.Unlock()
// 查找匹配的期望
for _, expectedCall := range m.ExpectedCalls {
if m.matchArguments(expectedCall.Arguments, arguments) {
// 记录调用
actualCall := Call{
Method: expectedCall.Method,
Arguments: arguments,
}
m.Calls = append(m.Calls, actualCall)
// 执行自定义函数
if expectedCall.RunFn != nil {
expectedCall.RunFn(Arguments(arguments))
}
// 等待指定时间
if expectedCall.WaitFor > 0 {
time.Sleep(expectedCall.WaitFor)
}
// 返回预设的返回值
return Arguments(expectedCall.ReturnArguments)
}
}
// 没有匹配的期望
panic(fmt.Sprintf("unexpected call: %v", arguments))
}
// AssertExpectations:验证所有期望
func (m *Mock) AssertExpectations(t TestingT) bool {
m.mutex.Lock()
defer m.mutex.Unlock()
success := true
for _, expectedCall := range m.ExpectedCalls {
actualCount := m.countCalls(expectedCall.Method, expectedCall.Arguments)
if expectedCall.Times > 0 && actualCount != expectedCall.Times {
t.Errorf("Expected %s to be called %d times, but was called %d times",
expectedCall.Method, expectedCall.Times, actualCount)
success = false
}
}
return success
}
// 辅助方法:匹配参数
func (m *Mock) matchArguments(expected, actual []interface{}) bool {
if len(expected) != len(actual) {
return false
}
for i := range expected {
// 实际实现会处理Anything等特殊匹配器
if expected[i] != actual[i] {
return false
}
}
return true
}
// 辅助方法:统计调用次数
func (m *Mock) countCalls(method string, arguments []interface{}) int {
count := 0
for _, call := range m.Calls {
if call.Method == method && m.matchArguments(arguments, call.Arguments) {
count++
}
}
return count
}
type TestingT interface {
Errorf(format string, args ...interface{})
Helper()
}
type Arguments []interface{}
func (a Arguments) Int(index int) int {
return a[index].(int)
}
func (a Arguments) String(index int) string {
return a[index].(string)
}
func (a Arguments) Get(index int) interface{} {
return a[index]
}
func (a Arguments) Error(index int) error {
obj := a[index]
if obj == nil {
return nil
}
return obj.(error)
}
---
b.线程安全设计
Mock框架使用mutex保证并发安全。所有修改ExpectedCalls和Calls列表的操作都在锁保护下进行,确保在并发测试场景下的正确性。这对于测试并发代码至关重要。
1.6 与标准库对比
01.功能对比
a.断言能力
a.标准库testing
Go标准库testing包提供基础的测试框架,但不包含断言功能。开发者需要手动使用if判断和t.Errorf记录错误,代码冗长且容易出错。每个断言需要3-5行代码,错误信息需要手动格式化。
b.testify优势
testify提供100+断言方法,一行代码完成断言。自动生成详细的错误信息,包括期望值、实际值、类型信息和差异对比。支持深度比较、正则匹配、集合操作等复杂断言场景。
c.对比示例
---
// 断言能力对比:标准库 vs testify
package comparison
import (
"testing"
"github.com/stretchr/testify/assert"
"reflect"
"strings"
"regexp"
)
// ========== 场景1:基础相等断言 ==========
// 标准库:需要手动判断和格式化错误
func TestEqualStdLib(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
// testify:一行代码完成
func TestEqualTestify(t *testing.T) {
result := Add(2, 3)
assert.Equal(t, 5, result)
}
// ========== 场景2:结构体比较 ==========
type Person struct {
Name string
Age int
}
// 标准库:需要逐字段比较
func TestStructStdLib(t *testing.T) {
expected := Person{Name: "Alice", Age: 25}
actual := GetPerson()
if actual.Name != expected.Name {
t.Errorf("Name = %s; want %s", actual.Name, expected.Name)
}
if actual.Age != expected.Age {
t.Errorf("Age = %d; want %d", actual.Age, expected.Age)
}
}
// testify:自动深度比较
func TestStructTestify(t *testing.T) {
expected := Person{Name: "Alice", Age: 25}
actual := GetPerson()
assert.Equal(t, expected, actual)
}
// ========== 场景3:切片比较 ==========
// 标准库:需要长度检查+元素遍历
func TestSliceStdLib(t *testing.T) {
expected := []int{1, 2, 3, 4, 5}
actual := GetNumbers()
if len(actual) != len(expected) {
t.Fatalf("length = %d; want %d", len(actual), len(expected))
}
for i := range expected {
if actual[i] != expected[i] {
t.Errorf("element[%d] = %d; want %d", i, actual[i], expected[i])
}
}
}
// testify:一行搞定
func TestSliceTestify(t *testing.T) {
expected := []int{1, 2, 3, 4, 5}
actual := GetNumbers()
assert.Equal(t, expected, actual)
}
// ========== 场景4:包含判断 ==========
// 标准库:需要手动遍历查找
func TestContainsStdLib(t *testing.T) {
list := []string{"apple", "banana", "cherry"}
target := "banana"
found := false
for _, item := range list {
if item == target {
found = true
break
}
}
if !found {
t.Errorf("list does not contain %s", target)
}
}
// testify:直接使用Contains断言
func TestContainsTestify(t *testing.T) {
list := []string{"apple", "banana", "cherry"}
assert.Contains(t, list, "banana")
}
// ========== 场景5:正则匹配 ==========
// 标准库:需要手动编译正则并匹配
func TestRegexpStdLib(t *testing.T) {
text := "[email protected]"
pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
matched, err := regexp.MatchString(pattern, text)
if err != nil {
t.Fatalf("regexp error: %v", err)
}
if !matched {
t.Errorf("%s does not match pattern %s", text, pattern)
}
}
// testify:一行正则断言
func TestRegexpTestify(t *testing.T) {
text := "[email protected]"
assert.Regexp(t, `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`, text)
}
// ========== 场景6:错误处理 ==========
// 标准库:需要手动判断nil
func TestErrorStdLib(t *testing.T) {
err := DoSomething()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
// testify:专用错误断言
func TestErrorTestify(t *testing.T) {
err := DoSomething()
assert.NoError(t, err)
// 检查特定错误
err2 := DoSomethingElse()
assert.Error(t, err2)
assert.EqualError(t, err2, "specific error message")
}
// ========== 场景7:类型检查 ==========
// 标准库:使用reflect包
func TestTypeStdLib(t *testing.T) {
var obj interface{} = "hello"
if reflect.TypeOf(obj).Kind() != reflect.String {
t.Errorf("type = %T; want string", obj)
}
}
// testify:专用类型断言
func TestTypeTestify(t *testing.T) {
var obj interface{} = "hello"
assert.IsType(t, "", obj)
}
// 辅助函数
func Add(a, b int) int { return a + b }
func GetPerson() Person { return Person{Name: "Alice", Age: 25} }
func GetNumbers() []int { return []int{1, 2, 3, 4, 5} }
func DoSomething() error { return nil }
func DoSomethingElse() error { return errors.New("specific error message") }
---
b.Mock能力
a.标准库不足
Go标准库不提供mock功能,开发者需要手动创建mock结构体、实现接口方法、记录调用信息、验证调用。这需要大量样板代码,且容易出错。
b.testify解决方案
testify的mock包提供完整的mock框架,通过嵌入mock.Mock结构体和调用On/Called/AssertExpectations方法,轻松实现接口mock。支持参数匹配、返回值控制、调用验证等高级功能。
02.代码量对比
a.测试代码行数
使用testify可以将测试代码量减少50-70%。断言代码从平均3-5行减少到1行,mock代码从几十行减少到10行以内。这不仅提高了开发效率,也增强了测试代码的可读性和可维护性。
b.实际项目对比
---
// 实际项目测试代码量对比
package project
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// ========== 完整功能测试对比 ==========
// 定义业务接口和结构
type UserService struct {
repo UserRepository
}
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
func (s *UserService) ValidateUser(user *User) error {
if user.Name == "" {
return errors.New("name is required")
}
if user.Age < 0 || user.Age > 150 {
return errors.New("invalid age")
}
return nil
}
type User struct {
ID int
Name string
Age int
}
type UserRepository interface {
FindByID(id int) (*User, error)
}
// ========== 标准库实现(约60行)==========
// 手动实现Mock对象
type MockUserRepositoryStdLib struct {
calls []MockCall
returnValues map[int]*User
returnErrors map[int]error
}
type MockCall struct {
method string
args []interface{}
}
func (m *MockUserRepositoryStdLib) FindByID(id int) (*User, error) {
m.calls = append(m.calls, MockCall{method: "FindByID", args: []interface{}{id}})
if err, ok := m.returnErrors[id]; ok {
return nil, err
}
if user, ok := m.returnValues[id]; ok {
return user, nil
}
return nil, errors.New("not found")
}
func (m *MockUserRepositoryStdLib) AssertCalled(t *testing.T, method string, expectedArgs ...interface{}) {
found := false
for _, call := range m.calls {
if call.method == method {
if len(call.args) == len(expectedArgs) {
match := true
for i := range call.args {
if call.args[i] != expectedArgs[i] {
match = false
break
}
}
if match {
found = true
break
}
}
}
}
if !found {
t.Errorf("expected method %s to be called with args %v", method, expectedArgs)
}
}
// 标准库测试(约40行)
func TestUserServiceStdLib(t *testing.T) {
// 创建mock对象
mockRepo := &MockUserRepositoryStdLib{
returnValues: make(map[int]*User),
returnErrors: make(map[int]error),
}
// 设置返回值
expectedUser := &User{ID: 1, Name: "Alice", Age: 25}
mockRepo.returnValues[1] = expectedUser
// 创建服务
service := &UserService{repo: mockRepo}
// 执行测试
user, err := service.GetUser(1)
// 手动断言
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if user == nil {
t.Fatal("expected user, got nil")
}
if user.ID != expectedUser.ID {
t.Errorf("ID = %d; want %d", user.ID, expectedUser.ID)
}
if user.Name != expectedUser.Name {
t.Errorf("Name = %s; want %s", user.Name, expectedUser.Name)
}
if user.Age != expectedUser.Age {
t.Errorf("Age = %d; want %d", user.Age, expectedUser.Age)
}
// 验证调用
mockRepo.AssertCalled(t, "FindByID", 1)
}
// ========== testify实现(约20行)==========
// testify Mock对象(嵌入mock.Mock)
type MockUserRepositoryTestify struct {
mock.Mock
}
func (m *MockUserRepositoryTestify) FindByID(id int) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
// testify测试(约15行)
func TestUserServiceTestify(t *testing.T) {
// 创建mock对象
mockRepo := new(MockUserRepositoryTestify)
expectedUser := &User{ID: 1, Name: "Alice", Age: 25}
// 设置期望
mockRepo.On("FindByID", 1).Return(expectedUser, nil)
// 创建服务并测试
service := &UserService{repo: mockRepo}
user, err := service.GetUser(1)
// testify断言(5行 vs 标准库15行)
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)
// 验证mock调用
mockRepo.AssertExpectations(t)
}
// ========== 代码量统计 ==========
/*
标准库方案:
- Mock实现:约50行
- 测试代码:约40行
- 总计:约90行
testify方案:
- Mock实现:约10行
- 测试代码:约15行
- 总计:约25行
代码减少:约72%(90行 -> 25行)
*/
---
03.错误信息对比
a.标准库输出
标准库的错误信息由开发者手动格式化,通常只包含简单的期望值和实际值。对于复杂对象的比较,很难直观看出差异。错误位置信息有时不准确,需要手动调试定位。
b.testify输出
testify自动生成详细的错误信息,包括类型信息、值对比、diff差异、调用堆栈等。对于结构体比较,会展示每个字段的差异。对于切片比较,会标注不同元素的位置。错误信息格式化美观,易于阅读。
c.输出示例
---
// 错误信息对比示例
package errorinfo
import (
"testing"
"github.com/stretchr/testify/assert"
)
type Product struct {
ID int
Name string
Price float64
Tags []string
}
// ========== 标准库错误信息 ==========
func TestProductStdLib(t *testing.T) {
expected := Product{
ID: 1,
Name: "Laptop",
Price: 999.99,
Tags: []string{"electronics", "computer"},
}
actual := Product{
ID: 1,
Name: "Laptop",
Price: 1099.99, // 价格不同
Tags: []string{"electronics"}, // 标签不同
}
// 标准库输出(不直观):
// product_test.go:25: products are not equal
if !reflect.DeepEqual(expected, actual) {
t.Errorf("products are not equal")
}
// 即使逐字段比较,信息也不够详细:
// product_test.go:30: Price = 1099.99; want 999.99
if expected.Price != actual.Price {
t.Errorf("Price = %f; want %f", actual.Price, expected.Price)
}
}
// ========== testify错误信息 ==========
func TestProductTestify(t *testing.T) {
expected := Product{
ID: 1,
Name: "Laptop",
Price: 999.99,
Tags: []string{"electronics", "computer"},
}
actual := Product{
ID: 1,
Name: "Laptop",
Price: 1099.99,
Tags: []string{"electronics"},
}
// testify输出(详细且直观):
/*
Error Trace: product_test.go:55
Error: Not equal:
expected: errorinfo.Product{
ID: 1,
Name: "Laptop",
Price: 999.99,
Tags: []string{"electronics", "computer"},
}
actual : errorinfo.Product{
ID: 1,
Name: "Laptop",
Price: 1099.99,
Tags: []string{"electronics"},
}
Diff:
--- Expected
+++ Actual
@@ -2,5 +2,5 @@
ID: (int) 1,
Name: (string) (len=6) "Laptop",
- Price: (float64) 999.99,
+ Price: (float64) 1099.99,
- Tags: ([]string) (len=2) ["electronics", "computer"]
+ Tags: ([]string) (len=1) ["electronics"]
Test: TestProductTestify
*/
assert.Equal(t, expected, actual)
}
// ========== 切片差异对比 ==========
func TestSliceDiff(t *testing.T) {
expected := []int{1, 2, 3, 4, 5}
actual := []int{1, 2, 9, 4, 5}
// testify清晰标注差异位置:
/*
Error: Not equal:
expected: []int{1, 2, 3, 4, 5}
actual : []int{1, 2, 9, 4, 5}
Diff:
--- Expected
+++ Actual
@@ -1,5 +1,5 @@
([]int) (len=5) {
(int) 1,
(int) 2,
- (int) 3,
+ (int) 9,
(int) 4,
(int) 5
*/
assert.Equal(t, expected, actual)
}
---
04.学习曲线对比
a.标准库
标准库testing包API简单,但需要掌握Go语言的错误处理、反射、接口等高级特性才能编写高质量测试。手动实现mock对象需要理解接口设计和依赖注入模式。初学者容易写出冗长且难以维护的测试代码。
b.testify
testify API丰富但学习曲线平缓。核心断言方法只有20个左右,命名直观易记。mock框架提供结构化的使用模式On-Called-Assert,容易上手。丰富的文档和示例降低了学习成本。
05.性能对比
a.性能开销
testify由于使用反射进行深度比较,性能略低于手写的if判断。但在测试场景下,这点性能开销完全可以忽略。测试的目标是正确性和可维护性,而非极致性能。
b.实际影响
在典型的测试套件中,testify的性能开销占总测试时间的比例不到1%。绝大部分测试时间消耗在业务逻辑执行、数据库操作、网络请求等方面。testify带来的开发效率提升远超过微小的性能损失。
1.7 生态系统
01.社区支持
a.开源社区
a.GitHub项目状态
testify托管在GitHub上,拥有超过22000个Star,是Go语言生态中最受欢迎的测试库。项目活跃度高,每周都有代码提交,Issue响应及时,Pull Request审核专业。维护团队稳定,版本发布规律,向后兼容性好。
b.贡献者生态
testify拥有数百名贡献者,来自全球各地的开发者。社区欢迎各种形式的贡献,包括bug修复、新功能开发、文档改进、示例补充等。贡献指南完善,代码审查严格,确保代码质量。
c.社区资源
---
// testify社区资源索引
package ecosystem
/*
========== 官方资源 ==========
1. GitHub仓库
地址:https://github.com/stretchr/testify
内容:源代码、Issue追踪、PR管理、Release发布
2. 官方文档
地址:https://pkg.go.dev/github.com/stretchr/testify
内容:API文档、包说明、使用示例
3. GoDoc文档
地址:各包的godoc页面
特点:自动生成、实时更新、示例完整
========== 社区资源 ==========
1. Stack Overflow
标签:testify, go-testing
内容:问题解答、最佳实践、常见问题
2. Reddit Go社区
频道:r/golang
内容:讨论、经验分享、技术交流
3. 博客教程
- 官方博客
- 技术博客
- 个人博客
内容:深度教程、实战案例、技巧分享
4. 视频教程
平台:YouTube、Bilibili等
内容:入门教程、高级技巧、实战演示
========== 学习路径 ==========
入门阶段:
1. 阅读官方README
2. 学习基础断言(assert包)
3. 完成简单的单元测试
进阶阶段:
1. 掌握require vs assert区别
2. 学习mock基础
3. 尝试测试套件
高级阶段:
1. 深入mock高级特性
2. 掌握自定义断言
3. 优化测试架构
========== 获取帮助 ==========
1. 查看官方示例代码
2. 搜索GitHub Issues
3. 在Stack Overflow提问
4. 参与社区讨论
5. 阅读源代码实现
*/
// 安装testify
/*
使用go get安装:
go get github.com/stretchr/testify
使用go mod管理:
go mod init myproject
go get github.com/stretchr/testify
在go.mod中引用:
require github.com/stretchr/testify v1.8.4
*/
// 导入testify包
import (
"testing"
// 断言包
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
// Mock包
"github.com/stretchr/testify/mock"
// 测试套件
"github.com/stretchr/testify/suite"
// HTTP测试
"github.com/stretchr/testify/http"
)
// 快速上手示例
func QuickStartExample(t *testing.T) {
// 1. 基础断言
assert.Equal(t, 5, Add(2, 3))
// 2. 错误处理
err := DoSomething()
assert.NoError(t, err)
// 3. 集合操作
list := []int{1, 2, 3, 4, 5}
assert.Contains(t, list, 3)
assert.Len(t, list, 5)
}
func Add(a, b int) int { return a + b }
func DoSomething() error { return nil }
---
b.企业采用
testify被众多知名企业和开源项目采用,包括Docker、Kubernetes、HashiCorp、Uber等。这些项目的测试代码为testify的可靠性和生产就绪性提供了有力证明。企业级项目的广泛使用也推动了testify的持续改进和功能增强。
02.扩展库和工具
a.官方扩展
a.http包
testify提供http包用于HTTP测试,封装了httptest包的功能,提供更便捷的HTTP断言方法。支持测试HTTP处理器、验证响应状态码、检查响应头、比较响应体等。
b.suite包
suite包提供测试套件功能,支持SetUp/TearDown生命周期钩子、测试夹具管理、子测试组织等高级特性。适合构建结构化的集成测试和大型测试套件。
b.第三方集成
a.代码生成工具
mockery是testify官方推荐的mock代码生成工具,可以自动为接口生成mock实现。通过分析接口定义,自动生成包含On、Called等方法的mock结构体,大幅减少手写mock代码的工作量。
b.集成示例
---
// mockery代码生成工具使用示例
package tools
/*
========== mockery安装 ==========
使用go install安装:
go install github.com/vektra/mockery/v2@latest
验证安装:
mockery --version
========== 使用mockery生成mock ==========
1. 定义接口
*/
// UserRepository接口定义
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
Delete(id int) error
FindAll() ([]*User, error)
}
/*
2. 生成mock代码
命令行方式:
mockery --name=UserRepository --output=mocks --outpkg=mocks
生成的mock文件(mocks/UserRepository.go):
*/
// 自动生成的mock代码(mockery生成)
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) FindByID(id int) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func (m *MockUserRepository) Save(user *User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockUserRepository) Delete(id int) error {
args := m.Called(id)
return args.Error(0)
}
func (m *MockUserRepository) FindAll() ([]*User, error) {
args := m.Called()
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*User), args.Error(1)
}
/*
3. 使用生成的mock
*/
func TestWithGeneratedMock(t *testing.T) {
// 创建mock对象
mockRepo := new(MockUserRepository)
// 设置期望
expectedUser := &User{ID: 1, Name: "Alice"}
mockRepo.On("FindByID", 1).Return(expectedUser, nil)
// 执行测试
service := &UserService{repo: mockRepo}
user, err := service.GetUser(1)
// 断言
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)
// 验证mock
mockRepo.AssertExpectations(t)
}
/*
========== mockery配置文件 ==========
创建.mockery.yaml配置文件:
with-expecter: true
dir: "mocks"
outpkg: "mocks"
packages:
github.com/myproject/repository:
interfaces:
UserRepository:
ProductRepository:
使用配置文件生成:
mockery
========== mockery高级特性 ==========
1. 生成带Expecter的mock
--with-expecter
支持类型安全的期望设置
2. 递归生成所有接口
--all --recursive
扫描整个项目生成所有mock
3. 指定输出目录
--output=./mocks
自定义mock文件存放位置
4. 生成测试文件
--testonly
在生成的文件中添加//go:build !test
*/
type User struct {
ID int
Name string
}
type UserService struct {
repo UserRepository
}
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
---
c.CI/CD集成
a.持续集成支持
testify完美集成主流CI/CD平台,包括GitHub Actions、GitLab CI、Travis CI、CircleCI等。测试结果可以直接显示在CI系统中,失败的测试会阻止代码合并。支持并行测试、测试覆盖率报告、测试结果缓存等高级特性。
b.集成配置示例
---
// CI/CD集成配置示例
package cicd
/*
========== GitHub Actions配置 ==========
文件:.github/workflows/test.yml
*/
/*
name: Test
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Get dependencies
run: |
go mod download
go get github.com/stretchr/testify
- name: Run tests
run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.txt
- name: Run tests with testify
run: |
go test -v ./... | grep -A 10 "FAIL"
*/
/*
========== GitLab CI配置 ==========
文件:.gitlab-ci.yml
*/
/*
image: golang:1.21
stages:
- test
- coverage
test:
stage: test
script:
- go mod download
- go test -v -race ./...
coverage: '/coverage: \d+.\d+% of statements/'
coverage:
stage: coverage
script:
- go test -coverprofile=coverage.out ./...
- go tool cover -html=coverage.out -o coverage.html
artifacts:
paths:
- coverage.html
*/
/*
========== Makefile测试命令 ==========
文件:Makefile
*/
/*
.PHONY: test test-verbose test-coverage test-race
# 运行所有测试
test:
go test ./...
# 详细输出
test-verbose:
go test -v ./...
# 生成覆盖率报告
test-coverage:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
@echo "Coverage report: coverage.html"
# 竞态检测
test-race:
go test -race ./...
# 运行特定测试
test-one:
go test -v -run $(TEST) ./...
# 使用示例:
# make test # 运行所有测试
# make test-verbose # 详细输出
# make test-coverage # 生成覆盖率
# make TEST=TestUser test-one # 运行特定测试
*/
/*
========== 测试脚本 ==========
文件:scripts/test.sh
*/
/*
#!/bin/bash
set -e
echo "==> Running tests..."
go test -v -race ./...
echo "==> Generating coverage report..."
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
echo "==> Coverage by package:"
go test -coverprofile=coverage.out ./... | grep coverage
echo "==> Checking test coverage threshold (80%)..."
coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
threshold=80
if (( $(echo "$coverage < $threshold" | bc -l) )); then
echo "ERROR: Coverage $coverage% is below threshold $threshold%"
exit 1
fi
echo "✓ All tests passed with $coverage% coverage"
*/
---
03.版本兼容性
a.Go版本支持
testify支持Go 1.13及以上版本,与最新的Go版本保持同步。遵循Go的兼容性承诺,不会引入破坏性变更。定期更新以支持Go语言的新特性和改进。
b.向后兼容
testify非常重视向后兼容性,新版本不会破坏现有测试代码。API稳定,核心功能保持不变,新功能通过新方法或包提供。版本升级平滑,风险低。
c.版本管理
---
// testify版本管理示例
package version
/*
========== go.mod版本管理 ==========
指定版本:
require github.com/stretchr/testify v1.8.4
使用最新版:
go get -u github.com/stretchr/testify
锁定版本:
go mod tidy
========== 版本历史 ==========
v1.8.x (2023):
- 改进错误信息显示
- 增强mock参数匹配
- 性能优化
v1.7.x (2021):
- 支持Go 1.13+
- 新增部分断言方法
- Bug修复
v1.6.x (2020):
- 稳定版本
- 广泛使用
========== 升级建议 ==========
1. 查看Release Notes
2. 在测试环境验证
3. 运行完整测试套件
4. 逐步升级
5. 监控CI结果
========== 依赖管理 ==========
使用go mod管理依赖:
go mod download
go mod verify
go mod tidy
查看依赖树:
go mod graph | grep testify
检查更新:
go list -m -u github.com/stretchr/testify
*/
---
04.替代方案对比
a.其他测试库
Go生态中还有其他测试库如GoConvey、Ginkgo、Gomega等。GoConvey提供BDD风格的测试和Web UI,Ginkgo/Gomega提供BDD测试框架。testify相比这些库更轻量、更贴近Go原生风格,学习成本更低,社区更大。
b.选型建议
对于大多数Go项目,testify是最佳选择。如果需要BDD风格测试,可以考虑Ginkgo。如果需要Web UI查看测试结果,可以考虑GoConvey。但testify的通用性、稳定性和社区支持使其成为首选。
1.8 版本特性
01.主要版本演进
a.v1.0-v1.5早期版本
a.核心功能建立
早期版本建立了testify的核心架构,包括assert包、require包、mock包的基本实现。提供了基础的断言方法如Equal、NotEqual、Nil、NotNil等,以及mock框架的On、Called、AssertExpectations机制。这一阶段奠定了testify的设计理念和使用模式。
b.社区培育
早期版本通过简洁的API和清晰的文档快速获得社区认可,成为Go测试领域的流行选择。众多开源项目开始采用testify,形成了良好的社区生态和反馈循环。
b.v1.6-v1.7稳定期
a.功能完善
v1.6和v1.7版本是testify的稳定版本,经过多年使用验证,功能成熟可靠。增加了更多断言方法,改进了错误信息显示,优化了mock框架的性能。这两个版本被大量生产环境采用,积累了丰富的实战经验。
b.生态扩展
这一时期出现了mockery等配套工具,Go module成为标准依赖管理方式,testify的集成和使用变得更加便捷。CI/CD集成方案成熟,测试最佳实践逐步形成。
c.版本特性
---
// v1.6-v1.7版本特性示例
package v16v17
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/mock"
)
// v1.6新增特性:Eventually和Never断言
func TestEventuallyFeature(t *testing.T) {
counter := 0
// Eventually:等待条件最终满足(最多等待1秒)
assert.Eventually(t, func() bool {
counter++
return counter >= 5
}, time.Second, 10*time.Millisecond, "counter应该最终达到5")
// Never:确保条件在指定时间内始终不满足
value := 0
assert.Never(t, func() bool {
return value > 10
}, 500*time.Millisecond, 10*time.Millisecond, "value不应该超过10")
}
// v1.7新增特性:ErrorIs和ErrorAs断言
func TestErrorFeatures(t *testing.T) {
// 自定义错误类型
var customErr = errors.New("custom error")
// ErrorIs:检查错误链中是否包含特定错误
wrappedErr := fmt.Errorf("wrapped: %w", customErr)
assert.ErrorIs(t, wrappedErr, customErr)
// ErrorAs:检查错误是否可以转换为特定类型
type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string {
return e.Message
}
myErr := &MyError{Code: 404, Message: "not found"}
wrappedMyErr := fmt.Errorf("failed: %w", myErr)
var target *MyError
assert.ErrorAs(t, wrappedMyErr, &target)
assert.Equal(t, 404, target.Code)
}
// v1.7改进:mock返回函数支持
func TestMockReturnFunc(t *testing.T) {
mockRepo := new(MockRepository)
// Return函数:根据参数动态返回不同值
mockRepo.On("Get", mock.Anything).Return(func(id int) string {
return fmt.Sprintf("item-%d", id)
}, nil)
result1, _ := mockRepo.Get(1)
assert.Equal(t, "item-1", result1)
result2, _ := mockRepo.Get(2)
assert.Equal(t, "item-2", result2)
mockRepo.AssertExpectations(t)
}
type MockRepository struct {
mock.Mock
}
func (m *MockRepository) Get(id int) (string, error) {
args := m.Called(id)
if fn, ok := args.Get(0).(func(int) string); ok {
return fn(id), args.Error(1)
}
return args.String(0), args.Error(1)
}
---
c.v1.8当前版本
a.现代化改进
v1.8是当前主流使用的版本,支持Go 1.13及以上版本。改进了错误信息的格式化输出,增强了diff显示,优化了大型对象的比较性能。增加了更多实用的断言方法,改进了mock框架的类型安全性。
b.Go生态适配
完全支持Go modules,与Go 1.18+的泛型特性兼容。改进了与Go标准库的集成,特别是testing.T和testing.B的支持。优化了在并发测试场景下的表现,修复了竞态条件问题。
c.核心改进
---
// v1.8版本核心改进示例
package v18
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"time"
)
// v1.8改进:更好的diff显示
func TestImprovedDiff(t *testing.T) {
type ComplexStruct struct {
ID int
Name string
Tags []string
Metadata map[string]interface{}
CreatedAt time.Time
}
expected := ComplexStruct{
ID: 1,
Name: "Test",
Tags: []string{"tag1", "tag2", "tag3"},
Metadata: map[string]interface{}{
"key1": "value1",
"key2": 123,
},
CreatedAt: time.Now(),
}
actual := ComplexStruct{
ID: 1,
Name: "Test",
Tags: []string{"tag1", "tag2"}, // 少一个tag
Metadata: map[string]interface{}{
"key1": "value1",
"key2": 456, // 值不同
},
CreatedAt: expected.CreatedAt,
}
// v1.8的diff显示更加清晰,精确指出差异位置
assert.Equal(t, expected, actual)
}
// v1.8新增:Greater/Less系列断言
func TestComparisonAssertions(t *testing.T) {
// Greater:大于
assert.Greater(t, 10, 5)
// GreaterOrEqual:大于等于
assert.GreaterOrEqual(t, 10, 10)
// Less:小于
assert.Less(t, 5, 10)
// LessOrEqual:小于等于
assert.LessOrEqual(t, 5, 5)
// Positive:正数
assert.Positive(t, 42)
// Negative:负数
assert.Negative(t, -42)
}
// v1.8改进:mock参数匹配增强
func TestEnhancedMockMatching(t *testing.T) {
mockService := new(MockService)
// MatchedBy:自定义匹配函数
mockService.On("Process", mock.MatchedBy(func(user *User) bool {
return user.Age >= 18 && user.Active
})).Return(nil)
// 只有满足条件的调用才会匹配
err1 := mockService.Process(&User{Age: 20, Active: true})
assert.NoError(t, err1)
// 不满足条件的调用会失败
// err2 := mockService.Process(&User{Age: 16, Active: true})
// 会panic: no matching expectation
mockService.AssertExpectations(t)
}
// v1.8新增:NotImplements断言
func TestImplementsAssertions(t *testing.T) {
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
var buffer bytes.Buffer
// Implements:检查是否实现接口
assert.Implements(t, (*Writer)(nil), &buffer)
assert.Implements(t, (*Reader)(nil), &buffer)
// NotImplements:检查是否未实现接口
type MyStruct struct{}
assert.NotImplements(t, (*Writer)(nil), &MyStruct{})
}
type User struct {
Age int
Active bool
}
type MockService struct {
mock.Mock
}
func (m *MockService) Process(user *User) error {
args := m.Called(user)
return args.Error(0)
}
---
02.功能特性对比
a.断言方法演进
a.基础断言增强
从最初的20多个断言方法增加到100+个方法。早期只有Equal、NotEqual、Nil等基础断言,后续版本不断增加Contains、ElementsMatch、Subset、Greater、Less等实用断言。每个版本都在保持向后兼容的前提下丰富断言能力。
b.错误处理改进
错误相关断言从简单的Error、NoError扩展到ErrorIs、ErrorAs、EqualError、ErrorContains等。适应Go 1.13+的错误包装机制,支持错误链检查和类型断言。错误信息显示也不断优化,提供更详细的诊断信息。
b.Mock功能增强
a.参数匹配进化
从简单的精确匹配发展到支持Anything、AnythingOfType、MatchedBy等灵活匹配器。增加了IsType、AnythingOfTypeArgument等类型检查匹配器。支持自定义匹配函数,可以实现复杂的参数验证逻辑。
b.调用控制完善
从基础的On-Return机制扩展到支持Once、Twice、Times、Maybe等调用次数控制。增加了Run自定义执行函数、WaitUntil等待控制。支持After、NotBefore等调用顺序验证。
c.演进示例
---
// Mock功能演进对比
package evolution
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)
// ========== 早期版本:基础Mock ==========
func TestBasicMockOldStyle(t *testing.T) {
mockRepo := new(MockUserRepository)
// 早期:只支持精确参数匹配和简单返回
mockRepo.On("Get", 1).Return(&User{ID: 1, Name: "Alice"}, nil)
user, err := mockRepo.Get(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
mockRepo.AssertExpectations(t)
}
// ========== 现代版本:高级Mock ==========
func TestAdvancedMockNewStyle(t *testing.T) {
mockRepo := new(MockUserRepository)
// 现代:支持灵活匹配、调用控制、自定义行为
mockRepo.On("Get", mock.MatchedBy(func(id int) bool {
return id > 0 && id < 1000
})).Run(func(args mock.Arguments) {
id := args.Int(0)
t.Logf("Get called with id=%d", id)
}).Return(func(id int) *User {
return &User{ID: id, Name: fmt.Sprintf("User-%d", id)}
}, nil).Times(3)
// 调用3次,每次返回不同用户
for i := 1; i <= 3; i++ {
user, err := mockRepo.Get(i)
assert.NoError(t, err)
assert.Equal(t, i, user.ID)
}
// 验证调用次数
mockRepo.AssertExpectations(t)
mockRepo.AssertNumberOfCalls(t, "Get", 3)
}
// ========== Mock链式调用增强 ==========
func TestMockChaining(t *testing.T) {
mockRepo := new(MockUserRepository)
// 链式设置多个期望
mockRepo.On("Get", 1).Return(&User{ID: 1}, nil).Once()
mockRepo.On("Get", 2).Return(&User{ID: 2}, nil).Once()
mockRepo.On("Get", mock.Anything).Return(nil, errors.New("not found")).Maybe()
// 第一次调用
user1, _ := mockRepo.Get(1)
assert.Equal(t, 1, user1.ID)
// 第二次调用
user2, _ := mockRepo.Get(2)
assert.Equal(t, 2, user2.ID)
// 第三次调用匹配Anything
user3, err := mockRepo.Get(999)
assert.Error(t, err)
assert.Nil(t, user3)
mockRepo.AssertExpectations(t)
}
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) Get(id int) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
if fn, ok := args.Get(0).(func(int) *User); ok {
return fn(id), args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
---
03.性能优化历程
a.早期性能
早期版本主要关注功能完整性和API易用性,性能优化不是首要目标。反射操作较多,大型对象比较时性能开销明显。但对于常规测试场景,性能已经足够。
b.持续优化
后续版本不断优化性能,减少不必要的反射调用,改进对象比较算法。增加缓存机制,避免重复的类型检查。优化并发场景下的锁竞争,提高并行测试性能。
c.性能基准
---
// testify性能优化对比基准测试
package benchmark
import (
"testing"
"github.com/stretchr/testify/assert"
)
// 基准测试:简单断言性能
func BenchmarkSimpleAssertion(b *testing.B) {
for i := 0; i < b.N; i++ {
assert.Equal(b, 5, 2+3)
}
}
// 基准测试:结构体比较性能
func BenchmarkStructAssertion(b *testing.B) {
user1 := User{ID: 1, Name: "Alice", Age: 25}
user2 := User{ID: 1, Name: "Alice", Age: 25}
b.ResetTimer()
for i := 0; i < b.N; i++ {
assert.Equal(b, user1, user2)
}
}
// 基准测试:切片比较性能
func BenchmarkSliceAssertion(b *testing.B) {
slice1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slice2 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
b.ResetTimer()
for i := 0; i < b.N; i++ {
assert.Equal(b, slice1, slice2)
}
}
// 基准测试:mock调用性能
func BenchmarkMockCall(b *testing.B) {
mockRepo := new(MockUserRepository)
mockRepo.On("Get", mock.Anything).Return(&User{ID: 1}, nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
mockRepo.Get(1)
}
}
/*
性能测试结果(参考值):
BenchmarkSimpleAssertion-8 20000000 85 ns/op
BenchmarkStructAssertion-8 5000000 320 ns/op
BenchmarkSliceAssertion-8 3000000 450 ns/op
BenchmarkMockCall-8 2000000 680 ns/op
结论:
1. 简单断言性能优秀,纳秒级延迟
2. 复杂对象比较有一定开销,但仍在可接受范围
3. Mock调用性能良好,适合大规模测试
4. 总体性能足以支撑生产级测试需求
*/
---
04.未来发展方向
a.Go泛型支持
随着Go 1.18引入泛型,testify未来可能提供类型安全的泛型断言方法。例如Equal[T](expected T, actual T),在编译时检查类型匹配。这将进一步提升API的类型安全性和IDE支持。
b.性能持续优化
继续优化反射性能,探索编译时代码生成方案。改进大型对象比较算法,减少内存分配。优化并发测试场景的性能表现。
c.生态系统扩展
加强与其他测试工具的集成,如性能测试、模糊测试等。改进mock代码生成工具,支持更多场景。增强CI/CD集成能力,提供更丰富的测试报告。
d.社区驱动创新
testify作为开源项目,未来发展方向将由社区需求驱动。鼓励社区贡献新功能、改进建议和最佳实践。保持API稳定性的同时,持续创新满足新的测试需求。
2 核心组件
2.1 汇总:4个
01.核心包概览
a.包组织结构
testify采用模块化设计,将功能划分为多个独立的包,每个包专注于特定的测试场景。核心包包括assert断言包、require断言包、mock模拟包、suite套件包、http测试包。这种设计使开发者可以按需引入,保持代码简洁。
b.包依赖关系
assert包是基础,require包在assert基础上添加致命性行为。mock包独立于断言包,提供完整的mock功能。suite包依赖testing标准库,通过反射机制管理测试生命周期。http包封装httptest,简化HTTP测试。各包职责清晰,耦合度低。
c.包导入示例
---
// testify核心包导入和使用示例
package overview
import (
"testing"
// 1. assert包:非致命性断言
"github.com/stretchr/testify/assert"
// 2. require包:致命性断言
"github.com/stretchr/testify/require"
// 3. mock包:模拟对象
"github.com/stretchr/testify/mock"
// 4. suite包:测试套件
"github.com/stretchr/testify/suite"
// 5. http包:HTTP测试(可选)
"github.com/stretchr/testify/http"
)
// ========== 包使用场景示例 ==========
// 场景1:使用assert包进行基础测试
func TestWithAssert(t *testing.T) {
// 创建断言对象
a := assert.New(t)
// 执行多个断言,即使某个失败也继续执行
a.Equal(5, Add(2, 3), "加法测试")
a.NotZero(10, "非零测试")
a.True(IsValid(), "布尔测试")
// 所有断言都会执行,最后汇总报告失败
}
// 场景2:使用require包检查前置条件
func TestWithRequire(t *testing.T) {
r := require.New(t)
// 检查前置条件,失败则立即终止
config := LoadConfig()
r.NotNil(config, "配置必须加载成功")
// 如果config为nil,这里的代码不会执行
r.Equal("localhost", config.Host)
r.Equal(3306, config.Port)
}
// 场景3:使用mock包创建模拟对象
func TestWithMock(t *testing.T) {
// 创建mock对象
mockRepo := new(MockRepository)
// 设置期望
mockRepo.On("Get", 1).Return("data", nil)
// 执行测试
result, err := mockRepo.Get(1)
// 断言结果
assert.NoError(t, err)
assert.Equal(t, "data", result)
// 验证mock期望
mockRepo.AssertExpectations(t)
}
// 场景4:使用suite包组织测试
type MyTestSuite struct {
suite.Suite
db *Database
repo *Repository
}
func (s *MyTestSuite) SetupSuite() {
s.db = ConnectDatabase()
}
func (s *MyTestSuite) SetupTest() {
s.repo = NewRepository(s.db)
}
func (s *MyTestSuite) TestSomething() {
result := s.repo.Query()
assert.NotNil(s.T(), result)
}
func (s *MyTestSuite) TearDownSuite() {
s.db.Close()
}
func TestMyTestSuite(t *testing.T) {
suite.Run(t, new(MyTestSuite))
}
// ========== 包组合使用示例 ==========
// 实际项目中通常组合使用多个包
func TestCombinedUsage(t *testing.T) {
// 1. require检查前置条件
r := require.New(t)
db := ConnectDatabase()
r.NotNil(db, "数据库连接必须成功")
defer db.Close()
// 2. mock创建依赖
mockCache := new(MockCache)
mockCache.On("Get", "key1").Return("value1", nil)
// 3. assert验证结果
a := assert.New(t)
service := NewService(db, mockCache)
result, err := service.Process("key1")
a.NoError(err)
a.Equal("value1", result)
// 4. 验证mock调用
mockCache.AssertExpectations(t)
}
// ========== 辅助类型定义 ==========
type MockRepository struct {
mock.Mock
}
func (m *MockRepository) Get(id int) (string, error) {
args := m.Called(id)
return args.String(0), args.Error(1)
}
type MockCache struct {
mock.Mock
}
func (m *MockCache) Get(key string) (string, error) {
args := m.Called(key)
return args.String(0), args.Error(1)
}
type Config struct {
Host string
Port int
}
type Database struct{}
type Repository struct{}
type Service struct{}
func Add(a, b int) int { return a + b }
func IsValid() bool { return true }
func LoadConfig() *Config { return &Config{Host: "localhost", Port: 3306} }
func ConnectDatabase() *Database { return &Database{} }
func (db *Database) Close() {}
func NewRepository(db *Database) *Repository { return &Repository{} }
func (r *Repository) Query() interface{} { return "result" }
func NewService(db *Database, cache *MockCache) *Service { return &Service{} }
func (s *Service) Process(key string) (string, error) { return "value1", nil }
---
02.功能定位对比
a.assert vs require
a.核心差异
assert包提供非致命性断言,断言失败时记录错误但继续执行。适用于需要检查多个条件的场景,一次性发现所有问题。require包提供致命性断言,断言失败时立即终止测试。适用于前置条件检查,避免在错误状态下继续执行导致panic或误导性错误。
b.选择建议
前置条件使用require,如数据库连接、配置加载、必需资源初始化。多个独立检查使用assert,如验证对象的多个字段、检查集合中的多个元素。可以在同一测试中混合使用两种断言,先用require检查前置条件,再用assert验证多个结果。
c.对比示例
---
// assert vs require选择示例
package comparison
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ========== 错误用法示例 ==========
// 错误1:前置条件使用assert(可能导致panic)
func TestBadAssertUsage(t *testing.T) {
a := assert.New(t)
// 如果db为nil,断言失败但继续执行
db := ConnectDatabase()
a.NotNil(db) // ❌ 错误:应该使用require
// db可能为nil,这里会panic!
db.Query("SELECT * FROM users")
}
// 错误2:多个独立检查使用require(只能发现第一个问题)
func TestBadRequireUsage(t *testing.T) {
r := require.New(t)
user := GetUser(1)
r.NotNil(user)
// 如果Name错误,后续检查不会执行
r.Equal("Alice", user.Name) // ❌ 第一个失败就终止
r.Equal(25, user.Age) // 看不到这个问题
r.True(user.Active) // 看不到这个问题
}
// ========== 正确用法示例 ==========
// 正确1:前置条件使用require
func TestCorrectRequireUsage(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// require检查前置条件
db := ConnectDatabase()
r.NotNil(db, "数据库连接必须成功")
defer db.Close()
// 确保db不为nil后,安全地使用
rows, err := db.Query("SELECT * FROM users")
r.NoError(err, "查询必须成功")
defer rows.Close()
// assert检查多个结果
a.True(rows.Next(), "至少有一条记录")
a.NotNil(rows, "结果集不为空")
}
// 正确2:多个独立检查使用assert
func TestCorrectAssertUsage(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// require检查对象存在
user := GetUser(1)
r.NotNil(user, "用户必须存在")
// assert检查所有字段,一次性发现所有问题
a.Equal("Alice", user.Name, "用户名检查")
a.Equal(25, user.Age, "年龄检查")
a.True(user.Active, "激活状态检查")
a.NotEmpty(user.Email, "邮箱检查")
// 即使某些断言失败,所有字段都会被检查
}
// 正确3:嵌套资源的正确处理
func TestNestedResourceHandling(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// 第一层:数据库连接
db := ConnectDatabase()
r.NotNil(db)
defer db.Close()
// 第二层:事务
tx, err := db.Begin()
r.NoError(err)
defer tx.Rollback()
// 第三层:执行操作
result, err := tx.Exec("INSERT INTO users VALUES (?, ?)", 1, "Alice")
r.NoError(err)
// 验证多个结果(使用assert)
affected, _ := result.RowsAffected()
a.Equal(int64(1), affected)
lastID, _ := result.LastInsertId()
a.Positive(lastID)
}
// ========== 实战场景 ==========
// 场景:HTTP处理器测试
func TestHTTPHandler(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// require:确保请求创建成功
req, err := http.NewRequest("GET", "/users/1", nil)
r.NoError(err)
r.NotNil(req)
// 执行请求
rr := httptest.NewRecorder()
handler := UserHandler{}
handler.ServeHTTP(rr, req)
// assert:检查多个响应属性
a.Equal(http.StatusOK, rr.Code, "状态码检查")
a.Contains(rr.Header().Get("Content-Type"), "application/json", "Content-Type检查")
a.NotEmpty(rr.Body.String(), "响应体检查")
a.JSONEq(`{"id":1,"name":"Alice"}`, rr.Body.String(), "JSON内容检查")
}
type User struct {
Name string
Age int
Active bool
Email string
}
type UserHandler struct{}
func (h UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"id":1,"name":"Alice"}`))
}
func GetUser(id int) *User {
return &User{Name: "Alice", Age: 25, Active: true, Email: "[email protected]"}
}
---
b.mock vs 手写mock
mock包提供结构化的mock框架,通过On-Called-Assert模式实现期望设置和验证。相比手写mock对象,代码量减少80%以上,且支持参数匹配、调用次数验证等高级功能。手写mock适合简单场景,mock包适合复杂依赖和精确验证。
c.suite vs 函数式测试
suite包将测试组织成结构体,支持SetUp/TearDown钩子和共享状态。适合需要复杂初始化、资源管理、多个相关测试的场景。传统函数式测试简单直接,适合独立的单元测试。suite增加了组织性,但也引入了一定复杂度。
03.使用频率统计
a.包使用优先级
assert包是最常用的包,几乎所有测试都会使用。require包在需要前置条件检查时使用,使用频率约为assert的30-40%。mock包在需要隔离依赖时使用,使用频率约20-30%。suite包在大型项目和集成测试中使用,使用频率约10-15%。http包在Web项目中使用,使用频率约5-10%。
b.项目类型差异
纯逻辑库项目主要使用assert和require包,mock使用较少。微服务项目大量使用mock包隔离外部依赖,suite包组织集成测试。Web应用项目广泛使用http包测试API,配合mock和suite。数据密集型项目使用suite管理数据库连接和测试数据。
c.学习路径建议
---
// testify学习路径建议
package learning
/*
========== 第一阶段:基础入门(1-2天)==========
目标:掌握基础断言,能编写简单单元测试
学习内容:
1. assert包基础
- Equal, NotEqual
- Nil, NotNil
- True, False
- NoError, Error
2. 基本测试编写
- 创建断言对象
- 编写测试函数
- 运行测试
实践项目:
- 为简单函数编写测试
- 测试数据结构操作
- 测试错误处理
========== 第二阶段:进阶应用(3-5天)==========
目标:掌握require、mock基础,能测试复杂业务逻辑
学习内容:
1. require包
- 理解致命性断言
- 前置条件检查
- 资源管理
2. mock包基础
- 创建mock对象
- On-Return模式
- AssertExpectations验证
3. 更多断言方法
- Contains, ElementsMatch
- Greater, Less
- Regexp, JSONEq
实践项目:
- 测试服务层代码
- mock数据库接口
- 测试业务规则
========== 第三阶段:高级特性(5-7天)==========
目标:掌握suite、高级mock,能构建完整测试体系
学习内容:
1. suite包
- 测试套件组织
- 生命周期钩子
- 测试夹具管理
2. mock高级特性
- 参数匹配器
- 调用次数控制
- 自定义行为
3. http包
- HTTP处理器测试
- API集成测试
实践项目:
- 构建集成测试套件
- 复杂mock场景
- API端到端测试
========== 第四阶段:最佳实践(持续)==========
目标:形成测试思维,建立团队测试规范
学习内容:
1. 测试组织架构
2. CI/CD集成
3. 测试覆盖率管理
4. 性能测试
5. 测试重构技巧
实践项目:
- 真实项目测试
- 团队规范建立
- 测试自动化
*/
---
04.包版本兼容性
a.稳定性保证
testify所有核心包保持API稳定,向后兼容性好。新版本不会破坏现有代码,新功能通过新方法提供。包之间版本同步,统一发布,避免版本冲突。
b.依赖管理
使用Go modules统一管理所有包的依赖。一次引入testify,所有包使用相同版本。go.mod自动处理版本锁定和依赖解析,确保构建可重现性。
2.2 assert包
01.包设计理念
a.非致命性断言
assert包的核心设计理念是非致命性断言,断言失败时记录错误但不终止测试执行。这使得一个测试函数可以执行多个断言,一次性发现所有问题,而不是每次只能看到第一个失败的断言。这种设计提高了测试的诊断效率。
b.链式API设计
assert包提供两种使用方式:直接调用包级别函数assert.Equal(t, expected, actual),或创建断言对象a := assert.New(t)然后调用a.Equal(expected, actual)。后者避免了重复传递testing.T参数,代码更简洁。
c.详细错误信息
断言失败时自动生成详细的错误信息,包括期望值、实际值、类型信息、差异对比等。对于复杂对象,提供格式化的diff输出,清晰标注差异位置。支持自定义错误消息,进一步增强可读性。
d.设计示例
---
// assert包设计理念示例
package assertdesign
import (
"testing"
"github.com/stretchr/testify/assert"
)
// ========== 使用方式1:包级别函数 ==========
func TestPackageLevel(t *testing.T) {
// 每次调用都传递t参数
assert.Equal(t, 5, Add(2, 3))
assert.NotNil(t, GetUser(1))
assert.True(t, IsValid())
assert.NoError(t, DoSomething())
// 优点:直观简单
// 缺点:需要重复传递t
}
// ========== 使用方式2:断言对象 ==========
func TestAssertionObject(t *testing.T) {
// 创建断言对象,只传递一次t
a := assert.New(t)
// 后续调用不需要传递t
a.Equal(5, Add(2, 3))
a.NotNil(GetUser(1))
a.True(IsValid())
a.NoError(DoSomething())
// 优点:代码简洁
// 缺点:需要额外一行创建对象
}
// ========== 非致命性演示 ==========
func TestNonFatal(t *testing.T) {
a := assert.New(t)
user := GetUser(1)
// 即使某个断言失败,所有断言都会执行
a.NotNil(user) // 断言1
a.Equal("Alice", user.Name) // 断言2:即使断言1失败,这个也会执行
a.Equal(25, user.Age) // 断言3:继续执行
a.True(user.Active) // 断言4:继续执行
a.NotEmpty(user.Email) // 断言5:继续执行
// 测试结束后会看到所有失败的断言,而不是只看到第一个
t.Log("测试继续执行到这里")
}
// ========== 错误信息演示 ==========
func TestDetailedErrorInfo(t *testing.T) {
a := assert.New(t)
type Product struct {
ID int
Name string
Price float64
Tags []string
}
expected := Product{
ID: 1,
Name: "Laptop",
Price: 999.99,
Tags: []string{"electronics", "computer"},
}
actual := Product{
ID: 1,
Name: "Laptop",
Price: 1099.99, // 不同
Tags: []string{"electronics"}, // 不同
}
// assert会生成详细的diff信息
a.Equal(expected, actual, "产品信息应该匹配")
/*
输出示例:
Error Trace: test.go:65
Error: Not equal:
expected: Product{ID:1, Name:"Laptop", Price:999.99, Tags:["electronics", "computer"]}
actual : Product{ID:1, Name:"Laptop", Price:1099.99, Tags:["electronics"]}
Diff:
--- Expected
+++ Actual
@@ -2,5 +2,5 @@
ID: 1,
Name: "Laptop",
- Price: 999.99,
+ Price: 1099.99,
- Tags: ["electronics", "computer"]
+ Tags: ["electronics"]
Messages: 产品信息应该匹配
Test: TestDetailedErrorInfo
*/
}
// ========== 自定义错误消息 ==========
func TestCustomMessages(t *testing.T) {
a := assert.New(t)
// 不带消息
a.Equal(5, Add(2, 3))
// 简单字符串消息
a.Equal(10, Add(5, 5), "5+5应该等于10")
// 格式化消息
x, y := 3, 4
a.Equal(7, Add(x, y), "Add(%d, %d)应该等于7", x, y)
// 复杂消息
user := GetUser(1)
a.NotNil(user, "用户ID=%d应该存在,当前系统状态=%s", 1, GetSystemStatus())
}
// 辅助函数
type User struct {
Name string
Age int
Active bool
Email string
}
func Add(a, b int) int { return a + b }
func GetUser(id int) *User { return &User{Name: "Alice", Age: 25, Active: true, Email: "[email protected]"} }
func IsValid() bool { return true }
func DoSomething() error { return nil }
func GetSystemStatus() string { return "running" }
---
02.核心断言方法
a.相等性断言
a.Equal系列
Equal是最常用的断言方法,使用reflect.DeepEqual进行深度比较,支持所有Go类型包括结构体、切片、map等。NotEqual检查不相等。Same检查指针相同,NotSame检查指针不同。
b.使用示例
---
// 相等性断言示例
package equality
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEqualityAssertions(t *testing.T) {
a := assert.New(t)
// ========== Equal:深度相等 ==========
// 基本类型
a.Equal(5, 2+3)
a.Equal("hello", "hel"+"lo")
a.Equal(3.14, 3.14)
// 结构体
type Point struct{ X, Y int }
a.Equal(Point{1, 2}, Point{1, 2})
// 切片
a.Equal([]int{1, 2, 3}, []int{1, 2, 3})
// map
a.Equal(
map[string]int{"a": 1, "b": 2},
map[string]int{"a": 1, "b": 2},
)
// 指针(比较指向的值,不是指针地址)
x, y := 42, 42
a.Equal(&x, &y) // 通过:值相等
// ========== NotEqual:不相等 ==========
a.NotEqual(5, 6)
a.NotEqual("hello", "world")
a.NotEqual([]int{1, 2}, []int{1, 3})
// ========== Same:指针相同 ==========
obj1 := &Point{1, 2}
obj2 := obj1
obj3 := &Point{1, 2}
a.Same(obj1, obj2) // 通过:指向同一对象
a.NotSame(obj1, obj3) // 通过:不同对象(虽然值相等)
// ========== EqualValues:类型转换后相等 ==========
// Equal会严格检查类型,EqualValues允许类型转换
var i int = 5
var i32 int32 = 5
// a.Equal(i, i32) // 失败:类型不同
a.EqualValues(i, i32) // 通过:值相等
// ========== InDelta:浮点数近似相等 ==========
// 浮点数精度问题
a.InDelta(0.1+0.2, 0.3, 0.0001) // 允许误差
// InDeltaSlice:切片中每个元素都近似相等
a.InDeltaSlice(
[]float64{0.1, 0.2, 0.3},
[]float64{0.10001, 0.20001, 0.30001},
0.001,
)
// InDeltaMapValues:map中每个值都近似相等
a.InDeltaMapValues(
map[string]float64{"a": 0.1, "b": 0.2},
map[string]float64{"a": 0.10001, "b": 0.20001},
0.001,
)
// InEpsilon:相对误差检查
a.InEpsilon(1000, 1001, 0.01) // 允许1%误差
}
---
b.存在性断言
a.Nil系列
Nil检查值为nil,NotNil检查值不为nil。支持指针、接口、slice、map、channel、function等可为nil的类型。Zero检查零值,NotZero检查非零值,适用于所有类型。Empty检查空集合或零值,NotEmpty检查非空。
b.使用示例
---
// 存在性断言示例
package existence
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestExistenceAssertions(t *testing.T) {
a := assert.New(t)
// ========== Nil / NotNil ==========
// 指针
var ptr *int = nil
a.Nil(ptr)
x := 42
a.NotNil(&x)
// 接口
var err error = nil
a.Nil(err)
err = errors.New("error")
a.NotNil(err)
// slice
var s []int = nil
a.Nil(s)
s = []int{}
a.NotNil(s) // 注意:空切片不是nil
// map
var m map[string]int = nil
a.Nil(m)
m = make(map[string]int)
a.NotNil(m)
// channel
var ch chan int = nil
a.Nil(ch)
ch = make(chan int)
a.NotNil(ch)
// ========== Zero / NotZero ==========
// 零值检查(适用于所有类型)
a.Zero(0)
a.Zero("")
a.Zero(false)
a.Zero(0.0)
// 非零值
a.NotZero(1)
a.NotZero("hello")
a.NotZero(true)
a.NotZero(3.14)
// 结构体零值
type Point struct{ X, Y int }
a.Zero(Point{})
a.NotZero(Point{X: 1})
// ========== Empty / NotEmpty ==========
// 字符串
a.Empty("")
a.NotEmpty("hello")
// 切片
a.Empty([]int{})
a.NotEmpty([]int{1, 2, 3})
// map
a.Empty(map[string]int{})
a.NotEmpty(map[string]int{"a": 1})
// channel(检查长度)
ch1 := make(chan int, 10)
a.Empty(ch1)
ch1 <- 1
a.NotEmpty(ch1)
// 零值也被视为empty
a.Empty(0)
a.Empty(false)
}
---
c.布尔断言
True和False检查布尔值。看似简单,但在测试复杂条件时非常有用。支持自定义错误消息,清楚说明被测试的条件。
---
// 布尔断言示例
package boolean
import (
"testing"
"github.com/stretchr/testify/assert"
"strings"
)
func TestBooleanAssertions(t *testing.T) {
a := assert.New(t)
// ========== True / False ==========
// 基础用法
a.True(true)
a.False(false)
// 条件表达式
a.True(5 > 3)
a.False(5 < 3)
// 函数返回值
a.True(IsValid())
a.False(IsExpired())
// 复杂条件
user := GetUser(1)
a.True(user.Age >= 18, "用户应该是成年人")
a.True(user.Active && user.Verified, "用户应该激活且已验证")
// 字符串检查
str := "hello world"
a.True(strings.Contains(str, "world"))
a.True(len(str) > 5)
// 集合检查
list := []int{1, 2, 3, 4, 5}
a.True(len(list) == 5)
a.True(list[0] == 1)
// 自定义验证函数
a.True(ValidateEmail("[email protected]"), "应该是有效的邮箱")
a.False(ValidateEmail("invalid"), "应该是无效的邮箱")
}
type User struct {
Age int
Active bool
Verified bool
}
func IsValid() bool { return true }
func IsExpired() bool { return false }
func GetUser(id int) User { return User{Age: 25, Active: true, Verified: true} }
func ValidateEmail(email string) bool {
return strings.Contains(email, "@")
}
---
03.集合断言方法
a.Contains系列
Contains检查集合包含某个元素,支持字符串、slice、array、map。NotContains检查不包含。Subset检查子集关系。ElementsMatch检查两个集合包含相同元素(忽略顺序)。
b.使用示例
---
// 集合断言示例
package collection
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCollectionAssertions(t *testing.T) {
a := assert.New(t)
// ========== Contains / NotContains ==========
// 字符串
a.Contains("hello world", "world")
a.NotContains("hello world", "goodbye")
// 切片
numbers := []int{1, 2, 3, 4, 5}
a.Contains(numbers, 3)
a.NotContains(numbers, 10)
// map(检查键)
m := map[string]int{"a": 1, "b": 2}
a.Contains(m, "a")
a.NotContains(m, "c")
// ========== Subset / NotSubset ==========
list := []int{1, 2, 3, 4, 5}
// 检查子集
a.Subset(list, []int{2, 3})
a.Subset(list, []int{1, 5})
a.NotSubset(list, []int{1, 10})
// ========== ElementsMatch ==========
// 忽略顺序的相等检查
a.ElementsMatch(
[]int{1, 2, 3},
[]int{3, 2, 1},
)
// 包含重复元素也能正确处理
a.ElementsMatch(
[]int{1, 2, 2, 3},
[]int{2, 3, 1, 2},
)
// 字符串切片
a.ElementsMatch(
[]string{"apple", "banana", "cherry"},
[]string{"cherry", "apple", "banana"},
)
// ========== Len ==========
// 检查长度
a.Len([]int{1, 2, 3}, 3)
a.Len("hello", 5)
a.Len(map[string]int{"a": 1, "b": 2}, 2)
ch := make(chan int, 5)
ch <- 1
ch <- 2
a.Len(ch, 2)
// ========== Unique ==========
// 检查元素唯一性
a.Unique([]int{1, 2, 3, 4}) // 通过
// a.Unique([]int{1, 2, 2, 3}) // 失败:有重复
// ========== Condition ==========
// 自定义条件检查集合
a.Condition(func() bool {
list := []int{1, 2, 3, 4, 5}
sum := 0
for _, v := range list {
sum += v
}
return sum == 15
}, "列表总和应该是15")
}
---
04.错误断言方法
a.Error系列
Error检查错误不为nil,NoError检查错误为nil。EqualError检查错误消息相等。ErrorIs检查错误链包含特定错误(Go 1.13+)。ErrorAs检查错误可转换为特定类型(Go 1.13+)。ErrorContains检查错误消息包含特定字符串。
b.使用示例
---
// 错误断言示例
package errorhandling
import (
"testing"
"github.com/stretchr/testify/assert"
"errors"
"fmt"
)
func TestErrorAssertions(t *testing.T) {
a := assert.New(t)
// ========== Error / NoError ==========
// 检查有错误
err1 := errors.New("something went wrong")
a.Error(err1)
// 检查无错误
err2 := DoSomethingSuccess()
a.NoError(err2)
// ========== EqualError ==========
// 检查错误消息精确匹配
err3 := errors.New("file not found")
a.EqualError(err3, "file not found")
// ========== ErrorContains ==========
// 检查错误消息包含子字符串
err4 := errors.New("failed to connect to database: timeout")
a.ErrorContains(err4, "database")
a.ErrorContains(err4, "timeout")
// ========== ErrorIs(Go 1.13+错误链)==========
// 定义标准错误
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")
// 包装错误
wrappedErr := fmt.Errorf("failed to get user: %w", ErrNotFound)
// 检查错误链
a.ErrorIs(wrappedErr, ErrNotFound) // 通过
a.NotErrorIs(wrappedErr, ErrPermission) // 通过
// 多层包装
doubleWrapped := fmt.Errorf("operation failed: %w", wrappedErr)
a.ErrorIs(doubleWrapped, ErrNotFound) // 仍然能检测到
// ========== ErrorAs(类型断言)==========
// 自定义错误类型
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// 创建并包装自定义错误
valErr := &ValidationError{Field: "email", Message: "invalid format"}
wrappedValErr := fmt.Errorf("validation failed: %w", valErr)
// 检查是否可转换为特定类型
var target *ValidationError
a.ErrorAs(wrappedValErr, &target)
a.Equal("email", target.Field)
a.Equal("invalid format", target.Message)
// ========== 实战示例:数据库操作 ==========
// 场景:数据库查询
user, err := GetUserByID(999)
if a.Error(err, "应该返回错误") {
// 只有err不为nil时才继续检查
a.ErrorIs(err, ErrNotFound)
a.Nil(user)
}
// 场景:成功操作
user2, err := GetUserByID(1)
a.NoError(err, "不应该有错误")
a.NotNil(user2, "应该返回用户对象")
}
func DoSomethingSuccess() error {
return nil
}
var ErrNotFound = errors.New("not found")
type User struct {
ID int
Name string
}
func GetUserByID(id int) (*User, error) {
if id == 999 {
return nil, fmt.Errorf("user query failed: %w", ErrNotFound)
}
return &User{ID: id, Name: "Alice"}, nil
}
---
2.3 require包
01.包设计理念
a.致命性断言
require包的核心特性是致命性断言,断言失败时立即终止当前测试函数的执行。这通过调用testing.T的FailNow方法实现,该方法会立即终止当前goroutine。require包适用于前置条件检查,当前置条件不满足时,继续执行测试没有意义且可能导致panic或产生误导性错误。
b.与assert包的关系
require包的API与assert包完全一致,所有assert包的方法在require包都有对应版本。实现上,require包在assert包基础上添加了FailNow调用。开发者可以根据语义选择使用assert或require,而无需学习不同的API。
c.使用场景
require最适合检查测试的前置条件,如资源初始化、数据准备、依赖创建等。如果这些前置条件不满足,后续测试代码会在错误状态下执行,产生大量无意义的错误信息甚至panic。使用require可以快速失败快速反馈,提高测试的诊断效率。
d.设计示例
---
// require包设计理念示例
package requiredesign
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
"database/sql"
)
// ========== 场景1:资源初始化 ==========
// 错误示例:使用assert(可能导致panic)
func TestBadResourceInit(t *testing.T) {
a := assert.New(t)
// 数据库连接失败,但测试继续
db, err := sql.Open("mysql", "invalid-connection")
a.NoError(err) // ❌ 失败但继续执行
// db可能为nil,这里会panic!
rows, _ := db.Query("SELECT * FROM users")
rows.Close()
}
// 正确示例:使用require(立即终止)
func TestGoodResourceInit(t *testing.T) {
r := require.New(t)
// 数据库连接失败,立即终止测试
db, err := sql.Open("mysql", "valid-connection")
r.NoError(err, "数据库连接必须成功") // ✅ 失败立即终止
defer db.Close()
// 确保db不为nil后才执行
rows, err := db.Query("SELECT * FROM users")
r.NoError(err)
defer rows.Close()
}
// ========== 场景2:对象存在性检查 ==========
// 错误示例:使用assert(可能访问nil)
func TestBadNilCheck(t *testing.T) {
a := assert.New(t)
config := LoadConfig()
a.NotNil(config) // ❌ 失败但继续
// config可能为nil,访问字段会panic
a.Equal("localhost", config.Host) // panic!
}
// 正确示例:使用require(安全终止)
func TestGoodNilCheck(t *testing.T) {
r := require.New(t)
a := assert.New(t)
config := LoadConfig()
r.NotNil(config, "配置必须加载成功") // ✅ 失败立即终止
// 确保config不为nil后才访问字段
a.Equal("localhost", config.Host)
a.Equal(3306, config.Port)
}
// ========== 场景3:多层依赖初始化 ==========
func TestMultiLayerInit(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// 第一层:配置
config := LoadConfig()
r.NotNil(config, "配置必须存在")
// 第二层:数据库
db, err := ConnectDB(config.DBHost)
r.NoError(err, "数据库连接必须成功")
r.NotNil(db)
defer db.Close()
// 第三层:仓库
repo := NewUserRepository(db)
r.NotNil(repo, "仓库初始化必须成功")
// 第四层:服务
service := NewUserService(repo)
r.NotNil(service, "服务初始化必须成功")
// 现在可以安全地测试业务逻辑(使用assert)
users, err := service.GetAllUsers()
a.NoError(err)
a.NotEmpty(users)
}
// ========== 场景4:测试数据准备 ==========
func TestDataPreparation(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// 准备测试数据(必须成功)
db := SetupTestDB(t)
r.NotNil(db)
defer CleanupTestDB(t, db)
// 插入测试数据(必须成功)
err := InsertTestUsers(db, []User{
{Name: "Alice", Age: 25},
{Name: "Bob", Age: 30},
})
r.NoError(err, "测试数据插入必须成功")
// 现在测试查询功能(可以用assert)
users, err := QueryUsers(db)
a.NoError(err)
a.Len(users, 2)
a.Equal("Alice", users[0].Name)
}
// ========== 场景5:文件操作 ==========
func TestFileOperations(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// 创建临时目录(必须成功)
tmpDir, err := os.MkdirTemp("", "test-*")
r.NoError(err, "临时目录创建必须成功")
defer os.RemoveAll(tmpDir)
// 创建测试文件(必须成功)
testFile := filepath.Join(tmpDir, "test.txt")
err = os.WriteFile(testFile, []byte("test content"), 0644)
r.NoError(err, "文件写入必须成功")
// 测试读取功能(可以用assert)
content, err := os.ReadFile(testFile)
a.NoError(err)
a.Equal("test content", string(content))
}
// 辅助类型定义
type Config struct {
Host string
Port int
DBHost string
}
type User struct {
Name string
Age int
}
func LoadConfig() *Config {
return &Config{Host: "localhost", Port: 8080, DBHost: "localhost:3306"}
}
func ConnectDB(host string) (*sql.DB, error) {
return nil, nil
}
func NewUserRepository(db *sql.DB) interface{} {
return &struct{}{}
}
func NewUserService(repo interface{}) interface{} {
return &struct{}{}
}
func SetupTestDB(t *testing.T) *sql.DB {
return nil
}
func CleanupTestDB(t *testing.T, db *sql.DB) {}
func InsertTestUsers(db *sql.DB, users []User) error {
return nil
}
func QueryUsers(db *sql.DB) ([]User, error) {
return []User{{Name: "Alice", Age: 25}, {Name: "Bob", Age: 30}}, nil
}
---
02.核心方法
a.API完全一致
require包提供的所有方法与assert包一一对应,包括Equal、NotEqual、Nil、NotNil、True、False、NoError、Error、Contains、Len等所有断言方法。唯一的区别是行为:assert继续执行,require立即终止。
b.方法对照
---
// require包核心方法示例
package requiremethods
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
)
func TestRequireMethods(t *testing.T) {
r := require.New(t)
// ========== 相等性方法 ==========
r.Equal(5, Add(2, 3))
r.NotEqual(5, Add(2, 2))
r.EqualValues(int32(5), int64(5))
// ========== 存在性方法 ==========
config := LoadConfig()
r.NotNil(config)
r.NotZero(config.Port)
r.NotEmpty(config.Host)
// ========== 布尔方法 ==========
r.True(config.Port > 0)
r.False(config.Host == "")
// ========== 集合方法 ==========
users := GetUsers()
r.Len(users, 3)
r.Contains(users, "Alice")
r.NotContains(users, "Unknown")
// ========== 错误方法 ==========
err := ValidateConfig(config)
r.NoError(err)
err2 := ValidateConfig(nil)
r.Error(err2)
r.ErrorContains(err2, "config is nil")
// ========== 类型方法 ==========
var obj interface{} = "hello"
r.IsType("", obj)
r.Implements((*fmt.Stringer)(nil), obj)
// ========== panic方法 ==========
r.Panics(func() {
panic("test panic")
})
r.NotPanics(func() {
_ = Add(2, 3)
})
}
// ========== require vs assert 方法对比 ==========
func TestRequireVsAssert(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// require:前置条件检查
db := ConnectDatabase()
r.NotNil(db) // 失败则终止
defer db.Close()
// assert:多个独立检查
users, err := db.QueryUsers()
a.NoError(err) // 失败继续
a.NotEmpty(users) // 失败继续
a.Len(users, 5) // 失败继续
a.Equal("Alice", users[0].Name) // 失败继续
// 可以看到所有失败的断言
}
// 辅助函数
func Add(a, b int) int { return a + b }
type Config struct {
Host string
Port int
}
func LoadConfig() *Config {
return &Config{Host: "localhost", Port: 8080}
}
func GetUsers() []string {
return []string{"Alice", "Bob", "Charlie"}
}
func ValidateConfig(config *Config) error {
if config == nil {
return errors.New("config is nil")
}
return nil
}
type Database struct{}
func ConnectDatabase() *Database {
return &Database{}
}
func (db *Database) Close() {}
func (db *Database) QueryUsers() ([]User, error) {
return []User{{Name: "Alice"}}, nil
}
---
03.实战模式
a.前置条件检查模式
在测试开始时使用require检查所有前置条件,确保测试环境正确初始化。包括资源创建、配置加载、依赖注入、测试数据准备等。这些步骤失败意味着测试无法正常进行,应该立即终止。
b.资源初始化模式
使用require确保资源初始化成功,配合defer进行资源清理。这是Go语言资源管理的标准模式,require确保资源可用,defer确保资源释放。
c.实战示例
---
// require包实战模式
package requirepatterns
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
"database/sql"
"context"
"time"
)
// ========== 模式1:完整的集成测试 ==========
func TestIntegrationPattern(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// 阶段1:环境准备(require)
config := LoadTestConfig()
r.NotNil(config, "测试配置必须加载")
// 阶段2:数据库连接(require)
db, err := sql.Open("postgres", config.DatabaseURL)
r.NoError(err, "数据库连接必须成功")
r.NotNil(db)
defer db.Close()
// 验证连接
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = db.PingContext(ctx)
r.NoError(err, "数据库ping必须成功")
// 阶段3:事务准备(require)
tx, err := db.BeginTx(ctx, nil)
r.NoError(err, "事务开始必须成功")
defer tx.Rollback()
// 阶段4:测试数据准备(require)
_, err = tx.ExecContext(ctx, "INSERT INTO users (name, email) VALUES ($1, $2)",
"Alice", "[email protected]")
r.NoError(err, "测试数据插入必须成功")
// 阶段5:执行测试(assert)
var name, email string
err = tx.QueryRowContext(ctx, "SELECT name, email FROM users WHERE name = $1", "Alice").
Scan(&name, &email)
a.NoError(err)
a.Equal("Alice", name)
a.Equal("[email protected]", email)
}
// ========== 模式2:HTTP服务测试 ==========
func TestHTTPServicePattern(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// 阶段1:启动测试服务器(require)
server := NewTestServer()
r.NotNil(server)
defer server.Close()
err := server.Start()
r.NoError(err, "服务器启动必须成功")
// 等待服务器就绪
r.Eventually(func() bool {
return server.IsReady()
}, 5*time.Second, 100*time.Millisecond, "服务器必须在5秒内就绪")
// 阶段2:创建HTTP客户端(require)
client := &http.Client{Timeout: 5 * time.Second}
r.NotNil(client)
// 阶段3:执行测试请求(assert)
resp, err := client.Get(server.URL + "/health")
a.NoError(err)
a.Equal(http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
a.NoError(err)
a.Contains(string(body), "healthy")
}
// ========== 模式3:文件系统测试 ==========
func TestFileSystemPattern(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// 阶段1:创建临时目录(require)
tmpDir, err := os.MkdirTemp("", "test-*")
r.NoError(err, "临时目录创建必须成功")
defer os.RemoveAll(tmpDir)
// 阶段2:创建子目录结构(require)
dataDir := filepath.Join(tmpDir, "data")
err = os.MkdirAll(dataDir, 0755)
r.NoError(err, "子目录创建必须成功")
// 阶段3:创建测试文件(require)
testFile := filepath.Join(dataDir, "test.json")
testData := []byte(`{"name":"Alice","age":25}`)
err = os.WriteFile(testFile, testData, 0644)
r.NoError(err, "测试文件创建必须成功")
// 验证文件存在
_, err = os.Stat(testFile)
r.NoError(err, "文件必须存在")
// 阶段4:测试文件操作(assert)
content, err := os.ReadFile(testFile)
a.NoError(err)
a.JSONEq(string(testData), string(content))
// 测试修改文件
newData := []byte(`{"name":"Bob","age":30}`)
err = os.WriteFile(testFile, newData, 0644)
a.NoError(err)
content, err = os.ReadFile(testFile)
a.NoError(err)
a.JSONEq(string(newData), string(content))
}
// ========== 模式4:并发测试 ==========
func TestConcurrencyPattern(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// 阶段1:创建并发安全的数据结构(require)
cache := NewSafeCache()
r.NotNil(cache)
// 阶段2:并发写入
const goroutines = 100
const itemsPerGoroutine = 10
done := make(chan bool, goroutines)
for i := 0; i < goroutines; i++ {
go func(id int) {
for j := 0; j < itemsPerGoroutine; j++ {
key := fmt.Sprintf("key-%d-%d", id, j)
cache.Set(key, id*itemsPerGoroutine+j)
}
done <- true
}(i)
}
// 等待所有goroutine完成
for i := 0; i < goroutines; i++ {
select {
case <-done:
case <-time.After(5 * time.Second):
r.Fail("并发写入超时")
}
}
// 阶段3:验证结果(assert)
expectedSize := goroutines * itemsPerGoroutine
a.Equal(expectedSize, cache.Size())
// 验证数据完整性
for i := 0; i < goroutines; i++ {
for j := 0; j < itemsPerGoroutine; j++ {
key := fmt.Sprintf("key-%d-%d", i, j)
value, exists := cache.Get(key)
a.True(exists, "键应该存在: %s", key)
a.Equal(i*itemsPerGoroutine+j, value)
}
}
}
// ========== 模式5:错误场景测试 ==========
func TestErrorScenariosPattern(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// 场景1:测试正常流程(作为基准)
t.Run("正常流程", func(t *testing.T) {
r := require.New(t)
a := assert.New(t)
service := NewUserService()
r.NotNil(service)
user, err := service.CreateUser("Alice", "[email protected]")
a.NoError(err)
a.NotNil(user)
a.Equal("Alice", user.Name)
})
// 场景2:测试参数验证错误
t.Run("空名称错误", func(t *testing.T) {
r := require.New(t)
a := assert.New(t)
service := NewUserService()
r.NotNil(service)
user, err := service.CreateUser("", "[email protected]")
a.Error(err)
a.Nil(user)
a.ErrorContains(err, "name is required")
})
// 场景3:测试资源不足错误
t.Run("数据库连接失败", func(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// 模拟数据库不可用
service := NewUserServiceWithDB(nil)
r.NotNil(service)
user, err := service.CreateUser("Alice", "[email protected]")
a.Error(err)
a.Nil(user)
a.ErrorContains(err, "database")
})
}
// 辅助类型定义
type TestConfig struct {
DatabaseURL string
}
type TestServer struct {
URL string
}
type SafeCache struct {
data map[string]int
mu sync.RWMutex
}
type UserService struct {
db interface{}
}
func LoadTestConfig() *TestConfig {
return &TestConfig{DatabaseURL: "postgres://localhost/test"}
}
func NewTestServer() *TestServer {
return &TestServer{URL: "http://localhost:8080"}
}
func (s *TestServer) Start() error {
return nil
}
func (s *TestServer) IsReady() bool {
return true
}
func (s *TestServer) Close() {}
func NewSafeCache() *SafeCache {
return &SafeCache{data: make(map[string]int)}
}
func (c *SafeCache) Set(key string, value int) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func (c *SafeCache) Get(key string) (int, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *SafeCache) Size() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.data)
}
func NewUserService() *UserService {
return &UserService{}
}
func NewUserServiceWithDB(db interface{}) *UserService {
return &UserService{db: db}
}
func (s *UserService) CreateUser(name, email string) (*User, error) {
if name == "" {
return nil, errors.New("name is required")
}
if s.db == nil {
return nil, errors.New("database not available")
}
return &User{Name: name}, nil
}
---
04.最佳实践
a.require优先原则
在测试开始时,优先使用require检查所有前置条件。确保测试环境正确后,再用assert进行业务逻辑验证。这种模式使测试更加健壮,错误信息更加清晰。
b.资源管理原则
所有通过require初始化的资源都应该配合defer进行清理。即使测试失败,defer也会执行,确保资源不泄漏。这是Go语言的最佳实践,require强化了这个模式。
c.错误信息原则
require断言失败会立即终止测试,因此错误消息特别重要。应该提供清晰的错误消息,说明失败的原因和影响。帮助开发者快速定位问题根源。
2.4 mock包
01.Mock框架概述
a.设计目标
testify的mock包提供完整的模拟对象框架,用于在测试中隔离外部依赖。通过mock,可以控制依赖的行为、验证方法调用、模拟各种场景包括成功、失败、超时等。Mock使单元测试真正成为单元测试,而不是集成测试。
b.核心机制
mock包的核心是Mock结构体,包含期望列表ExpectedCalls和实际调用列表Calls。通过On方法设置期望,Called方法记录调用,AssertExpectations方法验证期望。使用mutex保证线程安全,支持并发测试场景。
c.使用流程
Mock测试的标准流程包括四个步骤:定义接口、创建Mock对象、设置期望、执行测试、验证调用。这种结构化流程确保测试的完整性和可维护性。
d.基础示例
---
// mock包基础示例
package mockbasic
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
"errors"
)
// ========== 步骤1:定义接口 ==========
// 数据仓库接口
type UserRepository interface {
GetByID(id int) (*User, error)
Save(user *User) error
Delete(id int) error
FindAll() ([]*User, error)
}
// 业务服务
type UserService struct {
repo UserRepository
}
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.GetByID(id)
}
func (s *UserService) CreateUser(name, email string) error {
user := &User{Name: name, Email: email}
return s.repo.Save(user)
}
// ========== 步骤2:创建Mock对象 ==========
// Mock对象(嵌入mock.Mock)
type MockUserRepository struct {
mock.Mock
}
// 实现接口方法
func (m *MockUserRepository) GetByID(id int) (*User, error) {
// Called记录调用并返回预设值
args := m.Called(id)
// 处理nil返回值
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func (m *MockUserRepository) Save(user *User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockUserRepository) Delete(id int) error {
args := m.Called(id)
return args.Error(0)
}
func (m *MockUserRepository) FindAll() ([]*User, error) {
args := m.Called()
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*User), args.Error(1)
}
// ========== 步骤3:编写测试 ==========
// 测试1:基础Mock使用
func TestBasicMock(t *testing.T) {
// 创建mock对象
mockRepo := new(MockUserRepository)
// 设置期望:当调用GetByID(1)时返回指定用户
expectedUser := &User{ID: 1, Name: "Alice", Email: "[email protected]"}
mockRepo.On("GetByID", 1).Return(expectedUser, nil)
// 创建服务并注入mock
service := &UserService{repo: mockRepo}
// 执行测试
user, err := service.GetUser(1)
// 断言结果
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)
// 验证mock期望
mockRepo.AssertExpectations(t)
}
// 测试2:模拟错误场景
func TestMockError(t *testing.T) {
mockRepo := new(MockUserRepository)
// 设置期望:返回错误
mockRepo.On("GetByID", 999).Return(nil, errors.New("user not found"))
service := &UserService{repo: mockRepo}
user, err := service.GetUser(999)
// 验证错误处理
assert.Error(t, err)
assert.Nil(t, user)
assert.Contains(t, err.Error(), "not found")
mockRepo.AssertExpectations(t)
}
// 测试3:多次调用
func TestMultipleCalls(t *testing.T) {
mockRepo := new(MockUserRepository)
// 为不同参数设置不同期望
mockRepo.On("GetByID", 1).Return(&User{ID: 1, Name: "Alice"}, nil)
mockRepo.On("GetByID", 2).Return(&User{ID: 2, Name: "Bob"}, nil)
mockRepo.On("GetByID", 3).Return(nil, errors.New("not found"))
service := &UserService{repo: mockRepo}
// 测试第一次调用
user1, err1 := service.GetUser(1)
assert.NoError(t, err1)
assert.Equal(t, "Alice", user1.Name)
// 测试第二次调用
user2, err2 := service.GetUser(2)
assert.NoError(t, err2)
assert.Equal(t, "Bob", user2.Name)
// 测试第三次调用(错误场景)
user3, err3 := service.GetUser(3)
assert.Error(t, err3)
assert.Nil(t, user3)
mockRepo.AssertExpectations(t)
}
// 测试4:验证方法调用
func TestVerifyCall(t *testing.T) {
mockRepo := new(MockUserRepository)
newUser := &User{Name: "Charlie", Email: "[email protected]"}
mockRepo.On("Save", newUser).Return(nil)
service := &UserService{repo: mockRepo}
err := service.CreateUser("Charlie", "[email protected]")
assert.NoError(t, err)
// 验证Save方法被调用了一次,参数正确
mockRepo.AssertCalled(t, "Save", newUser)
mockRepo.AssertNumberOfCalls(t, "Save", 1)
}
// 数据类型定义
type User struct {
ID int
Name string
Email string
}
---
02.参数匹配器
a.匹配器类型
testify提供多种参数匹配器。Anything匹配任意值,AnythingOfType匹配特定类型,MatchedBy使用自定义函数匹配。这些匹配器增强了mock的灵活性,可以处理复杂的参数验证场景。
b.匹配器示例
---
// 参数匹配器详解
package matchers
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)
// ========== Anything匹配器 ==========
func TestAnythingMatcher(t *testing.T) {
mockRepo := new(MockUserRepository)
// Anything匹配任意参数
mockRepo.On("GetByID", mock.Anything).Return(&User{Name: "Default"}, nil)
// 任何ID都会匹配
user1, _ := mockRepo.GetByID(1)
assert.Equal(t, "Default", user1.Name)
user2, _ := mockRepo.GetByID(999)
assert.Equal(t, "Default", user2.Name)
mockRepo.AssertExpectations(t)
}
// ========== AnythingOfType匹配器 ==========
func TestAnythingOfTypeMatcher(t *testing.T) {
mockRepo := new(MockUserRepository)
// AnythingOfType匹配特定类型
mockRepo.On("Save", mock.AnythingOfType("*mockbasic.User")).Return(nil)
// 任何User指针都会匹配
err1 := mockRepo.Save(&User{Name: "Alice"})
assert.NoError(t, err1)
err2 := mockRepo.Save(&User{Name: "Bob"})
assert.NoError(t, err2)
mockRepo.AssertExpectations(t)
}
// ========== MatchedBy自定义匹配器 ==========
func TestMatchedByMatcher(t *testing.T) {
mockRepo := new(MockUserRepository)
// 自定义匹配函数:只匹配正数ID
mockRepo.On("GetByID", mock.MatchedBy(func(id int) bool {
return id > 0 && id < 1000
})).Return(&User{Name: "Valid ID"}, nil)
// 匹配成功
user1, _ := mockRepo.GetByID(1)
assert.Equal(t, "Valid ID", user1.Name)
user2, _ := mockRepo.GetByID(500)
assert.Equal(t, "Valid ID", user2.Name)
// 不匹配的调用会panic
// mockRepo.GetByID(1000) // panic
mockRepo.AssertExpectations(t)
}
// ========== 复杂匹配器示例 ==========
func TestComplexMatchers(t *testing.T) {
mockRepo := new(MockUserRepository)
// 匹配特定条件的User对象
mockRepo.On("Save", mock.MatchedBy(func(user *User) bool {
// 只接受年龄在18-65之间的用户
return user.Age >= 18 && user.Age <= 65 && user.Email != ""
})).Return(nil)
// 匹配成功
err1 := mockRepo.Save(&User{Name: "Alice", Age: 25, Email: "[email protected]"})
assert.NoError(t, err1)
// 不匹配(年龄太小)
// mockRepo.Save(&User{Name: "Bob", Age: 16, Email: "[email protected]"}) // panic
mockRepo.AssertExpectations(t)
}
// ========== 混合使用匹配器 ==========
func TestMixedMatchers(t *testing.T) {
mockService := new(MockService)
// 第一个参数精确匹配,第二个参数任意
mockService.On("Process", 1, mock.Anything).Return("result1", nil)
// 两个参数都用匹配器
mockService.On("Process",
mock.MatchedBy(func(id int) bool { return id > 100 }),
mock.AnythingOfType("string"),
).Return("result2", nil)
result1, _ := mockService.Process(1, "any value")
assert.Equal(t, "result1", result1)
result2, _ := mockService.Process(200, "test")
assert.Equal(t, "result2", result2)
mockService.AssertExpectations(t)
}
// Mock Service定义
type MockService struct {
mock.Mock
}
func (m *MockService) Process(id int, data string) (string, error) {
args := m.Called(id, data)
return args.String(0), args.Error(1)
}
type User struct {
Name string
Age int
Email string
}
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) GetByID(id int) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func (m *MockUserRepository) Save(user *User) error {
args := m.Called(user)
return args.Error(0)
}
---
03.调用控制
a.调用次数控制
testify提供多种调用次数控制方法。Once表示调用一次,Twice表示调用两次,Times(n)表示调用n次。Maybe表示可选调用。通过调用次数控制,可以精确验证方法被调用的情况。
b.调用行为控制
Run方法可以在调用时执行自定义函数,After方法可以延迟返回模拟超时,WaitUntil方法可以等待特定时间。这些方法让mock更加灵活,可以模拟各种复杂场景。
c.控制示例
---
// 调用控制示例
package callcontrol
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
"time"
)
// ========== 调用次数控制 ==========
func TestCallTimes(t *testing.T) {
mockRepo := new(MockUserRepository)
// Once:期望调用一次
mockRepo.On("GetByID", 1).Return(&User{Name: "Alice"}, nil).Once()
// Twice:期望调用两次
mockRepo.On("Save", mock.Anything).Return(nil).Twice()
// Times(n):期望调用n次
mockRepo.On("Delete", mock.Anything).Return(nil).Times(3)
// 执行调用
mockRepo.GetByID(1)
mockRepo.Save(&User{Name: "Bob"})
mockRepo.Save(&User{Name: "Charlie"})
mockRepo.Delete(1)
mockRepo.Delete(2)
mockRepo.Delete(3)
// 验证调用次数
mockRepo.AssertExpectations(t)
mockRepo.AssertNumberOfCalls(t, "GetByID", 1)
mockRepo.AssertNumberOfCalls(t, "Save", 2)
mockRepo.AssertNumberOfCalls(t, "Delete", 3)
}
// ========== Maybe:可选调用 ==========
func TestMaybeCall(t *testing.T) {
mockRepo := new(MockUserRepository)
// Maybe:调用或不调用都可以
mockRepo.On("GetByID", 1).Return(&User{Name: "Alice"}, nil).Maybe()
// 不调用也不会失败
mockRepo.AssertExpectations(t)
// 调用了也不会失败
mockRepo.GetByID(1)
mockRepo.AssertExpectations(t)
}
// ========== Run:自定义执行函数 ==========
func TestRunFunction(t *testing.T) {
mockRepo := new(MockUserRepository)
callCount := 0
// Run:每次调用时执行自定义函数
mockRepo.On("GetByID", mock.Anything).Run(func(args mock.Arguments) {
callCount++
id := args.Int(0)
t.Logf("GetByID called with id=%d, total calls=%d", id, callCount)
}).Return(&User{Name: "Dynamic"}, nil)
// 调用3次
mockRepo.GetByID(1)
mockRepo.GetByID(2)
mockRepo.GetByID(3)
assert.Equal(t, 3, callCount)
mockRepo.AssertExpectations(t)
}
// ========== After:延迟返回 ==========
func TestAfterDelay(t *testing.T) {
mockRepo := new(MockUserRepository)
// After:延迟100ms后返回(模拟慢查询)
mockRepo.On("GetByID", 1).
After(100 * time.Millisecond).
Return(&User{Name: "Slow"}, nil)
start := time.Now()
user, err := mockRepo.GetByID(1)
duration := time.Since(start)
assert.NoError(t, err)
assert.Equal(t, "Slow", user.Name)
assert.GreaterOrEqual(t, duration, 100*time.Millisecond)
mockRepo.AssertExpectations(t)
}
// ========== WaitUntil:等待通道 ==========
func TestWaitUntil(t *testing.T) {
mockRepo := new(MockUserRepository)
// 创建通道
ready := make(chan bool)
// WaitUntil:等待通道信号
mockRepo.On("GetByID", 1).
WaitUntil(ready).
Return(&User{Name: "Ready"}, nil)
// 在goroutine中延迟发送信号
go func() {
time.Sleep(50 * time.Millisecond)
close(ready)
}()
start := time.Now()
user, err := mockRepo.GetByID(1)
duration := time.Since(start)
assert.NoError(t, err)
assert.Equal(t, "Ready", user.Name)
assert.GreaterOrEqual(t, duration, 50*time.Millisecond)
mockRepo.AssertExpectations(t)
}
// ========== Return函数:动态返回值 ==========
func TestReturnFunction(t *testing.T) {
mockRepo := new(MockUserRepository)
// Return函数:根据参数动态生成返回值
mockRepo.On("GetByID", mock.Anything).Return(
func(id int) *User {
return &User{
ID: id,
Name: fmt.Sprintf("User-%d", id),
}
},
nil,
)
// 不同参数得到不同返回值
user1, _ := mockRepo.GetByID(1)
assert.Equal(t, 1, user1.ID)
assert.Equal(t, "User-1", user1.Name)
user2, _ := mockRepo.GetByID(100)
assert.Equal(t, 100, user2.ID)
assert.Equal(t, "User-100", user2.Name)
mockRepo.AssertExpectations(t)
}
// ========== 组合使用 ==========
func TestCombinedControl(t *testing.T) {
mockRepo := new(MockUserRepository)
callLog := []int{}
// 组合使用多个控制方法
mockRepo.On("GetByID", mock.Anything).
Run(func(args mock.Arguments) {
id := args.Int(0)
callLog = append(callLog, id)
}).
After(10 * time.Millisecond).
Return(func(id int) *User {
return &User{ID: id, Name: "User"}
}, nil).
Times(3)
// 调用3次
for i := 1; i <= 3; i++ {
start := time.Now()
user, _ := mockRepo.GetByID(i)
duration := time.Since(start)
assert.Equal(t, i, user.ID)
assert.GreaterOrEqual(t, duration, 10*time.Millisecond)
}
assert.Equal(t, []int{1, 2, 3}, callLog)
mockRepo.AssertExpectations(t)
}
type User struct {
ID int
Name string
}
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) GetByID(id int) (*User, error) {
args := m.Called(id)
if fn, ok := args.Get(0).(func(int) *User); ok {
return fn(id), args.Error(1)
}
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func (m *MockUserRepository) Save(user *User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockUserRepository) Delete(id int) error {
args := m.Called(id)
return args.Error(0)
}
---
04.验证方法
a.期望验证
AssertExpectations验证所有期望都被满足,AssertCalled验证特定方法被调用,AssertNotCalled验证方法未被调用。AssertNumberOfCalls验证调用次数。这些方法确保mock对象按预期被使用。
b.验证示例
---
// Mock验证方法详解
package verification
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)
func TestMockVerification(t *testing.T) {
mockRepo := new(MockUserRepository)
// 设置期望
mockRepo.On("GetByID", 1).Return(&User{Name: "Alice"}, nil)
mockRepo.On("Save", mock.Anything).Return(nil)
// 执行调用
mockRepo.GetByID(1)
mockRepo.Save(&User{Name: "Bob"})
// AssertExpectations:验证所有期望
mockRepo.AssertExpectations(t)
// AssertCalled:验证特定方法被调用
mockRepo.AssertCalled(t, "GetByID", 1)
mockRepo.AssertCalled(t, "Save", mock.Anything)
// AssertNumberOfCalls:验证调用次数
mockRepo.AssertNumberOfCalls(t, "GetByID", 1)
mockRepo.AssertNumberOfCalls(t, "Save", 1)
// AssertNotCalled:验证方法未被调用
mockRepo.AssertNotCalled(t, "Delete", mock.Anything)
}
type User struct{ Name string }
type MockUserRepository struct{ mock.Mock }
func (m *MockUserRepository) GetByID(id int) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func (m *MockUserRepository) Save(user *User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockUserRepository) Delete(id int) error {
args := m.Called(id)
return args.Error(0)
}
---
2.5 suite包
01.测试套件概念
a.设计目的
suite包提供测试套件功能,允许将相关测试组织成结构体,共享初始化逻辑和测试夹具。通过生命周期钩子函数SetUp和TearDown,可以在测试前后执行准备和清理工作。suite使测试代码更加结构化和可维护。
b.核心组件
Suite接口定义测试套件的基本结构,TestingSuite结构体提供T()方法访问testing.T。生命周期钩子包括SetupSuite、TearDownSuite、SetupTest、TearDownTest等。Run函数通过反射扫描并执行所有以Test开头的方法。
c.适用场景
suite适合需要复杂初始化的场景,如数据库连接、文件系统准备、外部服务启动等。适合有多个相关测试需要共享状态的场景。适合集成测试和端到端测试,不太适合简单的单元测试。
d.基础示例
---
// suite包基础示例
package suitebasic
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/stretchr/testify/assert"
"database/sql"
)
// ========== 步骤1:定义测试套件结构体 ==========
type UserRepositoryTestSuite struct {
suite.Suite // 嵌入suite.Suite
db *sql.DB // 共享资源
repo *UserRepository // 被测对象
}
// ========== 步骤2:实现生命周期钩子 ==========
// SetupSuite:整个套件开始前执行一次
func (s *UserRepositoryTestSuite) SetupSuite() {
// 连接测试数据库
var err error
s.db, err = sql.Open("mysql", "test:test@tcp(localhost:3306)/testdb")
s.Require().NoError(err, "数据库连接必须成功")
// 创建测试表
_, err = s.db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
email VARCHAR(100)
)
`)
s.Require().NoError(err)
}
// TearDownSuite:整个套件结束后执行一次
func (s *UserRepositoryTestSuite) TearDownSuite() {
// 清理测试表
s.db.Exec("DROP TABLE IF EXISTS users")
// 关闭数据库连接
if s.db != nil {
s.db.Close()
}
}
// SetupTest:每个测试方法前执行
func (s *UserRepositoryTestSuite) SetupTest() {
// 清空表数据
s.db.Exec("TRUNCATE TABLE users")
// 插入测试数据
s.db.Exec("INSERT INTO users (name, email) VALUES ('Alice', '[email protected]')")
s.db.Exec("INSERT INTO users (name, email) VALUES ('Bob', '[email protected]')")
// 创建被测对象
s.repo = NewUserRepository(s.db)
}
// TearDownTest:每个测试方法后执行
func (s *UserRepositoryTestSuite) TearDownTest() {
// 清理测试数据
s.db.Exec("TRUNCATE TABLE users")
}
// ========== 步骤3:编写测试方法 ==========
// 测试方法必须以Test开头
func (s *UserRepositoryTestSuite) TestFindByID() {
// 使用s.Assert()或s.Require()进行断言
user, err := s.repo.FindByID(1)
s.NoError(err) // 简写:s.Suite内置方法
s.NotNil(user)
s.Equal("Alice", user.Name)
s.Equal("[email protected]", user.Email)
}
func (s *UserRepositoryTestSuite) TestFindAll() {
users, err := s.repo.FindAll()
s.NoError(err)
s.Len(users, 2)
s.Equal("Alice", users[0].Name)
s.Equal("Bob", users[1].Name)
}
func (s *UserRepositoryTestSuite) TestCreate() {
newUser := &User{
Name: "Charlie",
Email: "[email protected]",
}
err := s.repo.Create(newUser)
s.NoError(err)
s.NotZero(newUser.ID)
// 验证数据库中存在
savedUser, err := s.repo.FindByID(newUser.ID)
s.NoError(err)
s.Equal("Charlie", savedUser.Name)
}
func (s *UserRepositoryTestSuite) TestUpdate() {
user, _ := s.repo.FindByID(1)
user.Email = "[email protected]"
err := s.repo.Update(user)
s.NoError(err)
// 验证更新成功
updated, _ := s.repo.FindByID(1)
s.Equal("[email protected]", updated.Email)
}
func (s *UserRepositoryTestSuite) TestDelete() {
err := s.repo.Delete(1)
s.NoError(err)
// 验证已删除
user, err := s.repo.FindByID(1)
s.Error(err)
s.Nil(user)
}
// ========== 步骤4:运行测试套件 ==========
// 测试入口函数
func TestUserRepositoryTestSuite(t *testing.T) {
suite.Run(t, new(UserRepositoryTestSuite))
}
// ========== 辅助定义 ==========
type User struct {
ID int
Name string
Email string
}
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) FindByID(id int) (*User, error) {
user := &User{}
err := r.db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", id).
Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
return user, nil
}
func (r *UserRepository) FindAll() ([]*User, error) {
rows, err := r.db.Query("SELECT id, name, email FROM users")
if err != nil {
return nil, err
}
defer rows.Close()
var users []*User
for rows.Next() {
user := &User{}
rows.Scan(&user.ID, &user.Name, &user.Email)
users = append(users, user)
}
return users, nil
}
func (r *UserRepository) Create(user *User) error {
result, err := r.db.Exec(
"INSERT INTO users (name, email) VALUES (?, ?)",
user.Name, user.Email,
)
if err != nil {
return err
}
id, _ := result.LastInsertId()
user.ID = int(id)
return nil
}
func (r *UserRepository) Update(user *User) error {
_, err := r.db.Exec(
"UPDATE users SET name=?, email=? WHERE id=?",
user.Name, user.Email, user.ID,
)
return err
}
func (r *UserRepository) Delete(id int) error {
_, err := r.db.Exec("DELETE FROM users WHERE id = ?", id)
return err
}
---
02.生命周期钩子
a.钩子执行顺序
生命周期钩子按特定顺序执行:SetupSuite→SetupTest→Test方法→TearDownTest→下一个Test→TearDownSuite。SetupSuite和TearDownSuite每个套件只执行一次,SetupTest和TearDownTest每个测试方法都执行。
b.钩子函数详解
---
// 生命周期钩子详解
package lifecycle
import (
"testing"
"github.com/stretchr/testify/suite"
"fmt"
)
// ========== 完整的生命周期演示 ==========
type LifecycleTestSuite struct {
suite.Suite
counter int
}
// SetupSuite:套件级别初始化
func (s *LifecycleTestSuite) SetupSuite() {
fmt.Println("1. SetupSuite: 整个套件开始前执行(只执行1次)")
// 适合:数据库连接、外部服务启动、全局配置加载
}
// SetupTest:测试级别初始化
func (s *LifecycleTestSuite) SetupTest() {
s.counter = 0
fmt.Println("2. SetupTest: 每个测试方法前执行")
// 适合:清空数据库、重置状态、准备测试数据
}
// BeforeTest:在SetupTest之后、测试方法之前执行
func (s *LifecycleTestSuite) BeforeTest(suiteName, testName string) {
fmt.Printf("3. BeforeTest: %s.%s 即将开始\n", suiteName, testName)
// 适合:记录日志、设置上下文、特定测试的准备
}
// 测试方法1
func (s *LifecycleTestSuite) TestExample1() {
fmt.Println("4. TestExample1: 测试方法执行")
s.counter++
s.Equal(1, s.counter)
}
// 测试方法2
func (s *LifecycleTestSuite) TestExample2() {
fmt.Println("4. TestExample2: 测试方法执行")
s.counter++
s.Equal(1, s.counter) // counter在SetupTest中重置为0
}
// AfterTest:在测试方法之后、TearDownTest之前执行
func (s *LifecycleTestSuite) AfterTest(suiteName, testName string) {
fmt.Printf("5. AfterTest: %s.%s 已完成\n", suiteName, testName)
// 适合:收集测试指标、验证后置条件
}
// TearDownTest:测试级别清理
func (s *LifecycleTestSuite) TearDownTest() {
fmt.Println("6. TearDownTest: 每个测试方法后执行")
// 适合:清理测试数据、释放测试资源
}
// TearDownSuite:套件级别清理
func (s *LifecycleTestSuite) TearDownSuite() {
fmt.Println("7. TearDownSuite: 整个套件结束后执行(只执行1次)")
// 适合:关闭数据库连接、停止外部服务、清理全局资源
}
func TestLifecycleTestSuite(t *testing.T) {
suite.Run(t, new(LifecycleTestSuite))
}
/*
输出示例:
1. SetupSuite: 整个套件开始前执行(只执行1次)
2. SetupTest: 每个测试方法前执行
3. BeforeTest: LifecycleTestSuite.TestExample1 即将开始
4. TestExample1: 测试方法执行
5. AfterTest: LifecycleTestSuite.TestExample1 已完成
6. TearDownTest: 每个测试方法后执行
2. SetupTest: 每个测试方法前执行
3. BeforeTest: LifecycleTestSuite.TestExample2 即将开始
4. TestExample2: 测试方法执行
5. AfterTest: LifecycleTestSuite.TestExample2 已完成
6. TearDownTest: 每个测试方法后执行
7. TearDownSuite: 整个套件结束后执行(只执行1次)
*/
// ========== 实战示例:数据库测试套件 ==========
type DatabaseTestSuite struct {
suite.Suite
db *sql.DB
tx *sql.Tx
cleanups []func()
}
func (s *DatabaseTestSuite) SetupSuite() {
// 连接测试数据库
var err error
s.db, err = sql.Open("postgres", "postgres://localhost/testdb")
s.Require().NoError(err)
// 运行迁移
s.runMigrations()
}
func (s *DatabaseTestSuite) SetupTest() {
// 每个测试使用独立事务
var err error
s.tx, err = s.db.Begin()
s.Require().NoError(err)
// 清空所有表
s.tx.Exec("TRUNCATE TABLE users CASCADE")
s.tx.Exec("TRUNCATE TABLE orders CASCADE")
// 重置清理函数列表
s.cleanups = nil
}
func (s *DatabaseTestSuite) TearDownTest() {
// 回滚事务(隔离测试)
if s.tx != nil {
s.tx.Rollback()
}
// 执行注册的清理函数
for i := len(s.cleanups) - 1; i >= 0; i-- {
s.cleanups[i]()
}
}
func (s *DatabaseTestSuite) TearDownSuite() {
// 关闭数据库连接
if s.db != nil {
s.db.Close()
}
}
// 辅助方法:注册清理函数
func (s *DatabaseTestSuite) AddCleanup(fn func()) {
s.cleanups = append(s.cleanups, fn)
}
func (s *DatabaseTestSuite) runMigrations() {
// 创建表结构
}
// ========== 实战示例:HTTP服务测试套件 ==========
type HTTPServiceTestSuite struct {
suite.Suite
server *httptest.Server
client *http.Client
}
func (s *HTTPServiceTestSuite) SetupSuite() {
// 启动测试服务器
handler := NewHTTPHandler()
s.server = httptest.NewServer(handler)
// 创建HTTP客户端
s.client = &http.Client{
Timeout: 10 * time.Second,
}
}
func (s *HTTPServiceTestSuite) TearDownSuite() {
// 关闭测试服务器
if s.server != nil {
s.server.Close()
}
}
func (s *HTTPServiceTestSuite) SetupTest() {
// 每个测试前清理session/cookie
jar, _ := cookiejar.New(nil)
s.client.Jar = jar
}
func NewHTTPHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
}
---
03.子测试支持
a.Run方法
suite支持使用T().Run创建子测试,可以在套件内部进一步组织测试。子测试可以并行执行,提高测试效率。子测试有独立的名称,便于识别失败的具体场景。
b.子测试示例
---
// 子测试支持示例
package subtest
import (
"testing"
"github.com/stretchr/testify/suite"
)
type SubTestSuite struct {
suite.Suite
}
// 使用子测试组织多个场景
func (s *SubTestSuite) TestUserValidation() {
// 场景1:有效用户
s.Run("ValidUser", func() {
user := &User{Name: "Alice", Email: "[email protected]"}
err := ValidateUser(user)
s.NoError(err)
})
// 场景2:空名称
s.Run("EmptyName", func() {
user := &User{Name: "", Email: "[email protected]"}
err := ValidateUser(user)
s.Error(err)
s.Contains(err.Error(), "name")
})
// 场景3:无效邮箱
s.Run("InvalidEmail", func() {
user := &User{Name: "Alice", Email: "invalid"}
err := ValidateUser(user)
s.Error(err)
s.Contains(err.Error(), "email")
})
}
// 表格驱动测试与子测试结合
func (s *SubTestSuite) TestCalculator() {
tests := []struct {
name string
a, b int
expected int
}{
{"Positive", 2, 3, 5},
{"Negative", -1, -2, -3},
{"Zero", 0, 5, 5},
{"Mixed", -3, 5, 2},
}
for _, tt := range tests {
s.Run(tt.name, func() {
result := Add(tt.a, tt.b)
s.Equal(tt.expected, result)
})
}
}
func TestSubTestSuite(t *testing.T) {
suite.Run(t, new(SubTestSuite))
}
type User struct {
Name string
Email string
}
func ValidateUser(user *User) error {
if user.Name == "" {
return errors.New("name is required")
}
if !strings.Contains(user.Email, "@") {
return errors.New("invalid email")
}
return nil
}
func Add(a, b int) int {
return a + b
}
---
04.最佳实践
a.资源管理
在SetupSuite中初始化昂贵资源如数据库连接,在TearDownSuite中清理。在SetupTest中准备测试数据,在TearDownTest中清理。使用defer确保资源一定被释放。合理使用生命周期钩子可以避免资源泄漏和测试间干扰。
b.测试隔离
每个测试应该独立,不依赖其他测试的执行顺序或结果。使用事务回滚或数据清理确保测试间隔离。避免在测试间共享可变状态。suite的SetupTest/TearDownTest机制有助于实现测试隔离。
c.命名规范
测试套件名称以TestSuite结尾,如UserRepositoryTestSuite。测试方法名称以Test开头,清晰描述测试内容,如TestCreateUser。生命周期钩子使用标准名称不要自定义。
2.6 http包
01.HTTP测试工具
a.包设计目的
testify的http包封装了Go标准库的httptest包,提供更便捷的HTTP测试断言。虽然http包在testify中使用频率不如assert和mock高,但对于Web应用和API服务测试仍然很有价值。它简化了HTTP请求和响应的测试代码。
b.核心功能
http包提供用于测试HTTP处理器的工具函数,可以创建测试请求、捕获响应、验证状态码和响应内容。与httptest.ResponseRecorder配合使用,可以完整地测试HTTP处理逻辑而无需启动真实服务器。
c.使用场景
http包适用于单元测试HTTP处理器函数,测试RESTful API端点,验证中间件行为,测试HTTP客户端代码。对于复杂的集成测试,可能需要配合httptest.Server使用。
d.基础示例
---
// http包基础示例
package httpbasic
import (
"testing"
"net/http"
"net/http/httptest"
"github.com/stretchr/testify/assert"
"encoding/json"
)
// ========== HTTP处理器定义 ==========
// 简单的GET处理器
func HelloHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, World!"))
}
// JSON API处理器
func UserHandler(w http.ResponseWriter, r *http.Request) {
user := map[string]interface{}{
"id": 1,
"name": "Alice",
"email": "[email protected]",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(user)
}
// POST处理器
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPOST {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
var user struct {
Name string `json:"name"`
Email string `json:"email"`
}
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"})
return
}
if user.Name == "" || user.Email == "" {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "Name and email required"})
return
}
response := map[string]interface{}{
"id": 123,
"name": user.Name,
"email": user.Email,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(response)
}
// ========== 测试示例 ==========
// 测试1:简单GET请求
func TestHelloHandler(t *testing.T) {
// 创建请求
req, err := http.NewRequest("GET", "/hello", nil)
assert.NoError(t, err)
// 创建ResponseRecorder捕获响应
rr := httptest.NewRecorder()
// 调用处理器
handler := http.HandlerFunc(HelloHandler)
handler.ServeHTTP(rr, req)
// 验证响应
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "text/plain", rr.Header().Get("Content-Type"))
assert.Equal(t, "Hello, World!", rr.Body.String())
}
// 测试2:JSON响应
func TestUserHandler(t *testing.T) {
req, _ := http.NewRequest("GET", "/user/1", nil)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(UserHandler)
handler.ServeHTTP(rr, req)
// 验证状态码和Content-Type
assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Header().Get("Content-Type"), "application/json")
// 解析JSON响应
var user map[string]interface{}
err := json.Unmarshal(rr.Body.Bytes(), &user)
assert.NoError(t, err)
// 验证JSON内容
assert.Equal(t, float64(1), user["id"])
assert.Equal(t, "Alice", user["name"])
assert.Equal(t, "[email protected]", user["email"])
// 或使用JSONEq直接比较JSON字符串
expected := `{"id":1,"name":"Alice","email":"[email protected]"}`
assert.JSONEq(t, expected, rr.Body.String())
}
// 测试3:POST请求
func TestCreateUserHandler(t *testing.T) {
// 准备请求体
requestBody := `{"name":"Bob","email":"[email protected]"}`
req, _ := http.NewRequest("POST", "/users", strings.NewReader(requestBody))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(CreateUserHandler)
handler.ServeHTTP(rr, req)
// 验证响应
assert.Equal(t, http.StatusCreated, rr.Code)
var response map[string]interface{}
json.Unmarshal(rr.Body.Bytes(), &response)
assert.Equal(t, float64(123), response["id"])
assert.Equal(t, "Bob", response["name"])
assert.Equal(t, "[email protected]", response["email"])
}
// 测试4:错误处理
func TestCreateUserHandler_InvalidJSON(t *testing.T) {
// 无效的JSON
req, _ := http.NewRequest("POST", "/users", strings.NewReader("invalid json"))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(CreateUserHandler)
handler.ServeHTTP(rr, req)
// 验证错误响应
assert.Equal(t, http.StatusBadRequest, rr.Code)
var errorResp map[string]string
json.Unmarshal(rr.Body.Bytes(), &errorResp)
assert.Equal(t, "Invalid JSON", errorResp["error"])
}
// 测试5:缺少必填字段
func TestCreateUserHandler_MissingFields(t *testing.T) {
requestBody := `{"name":"Bob"}` // 缺少email
req, _ := http.NewRequest("POST", "/users", strings.NewReader(requestBody))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(CreateUserHandler)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "required")
}
// 测试6:方法不允许
func TestCreateUserHandler_MethodNotAllowed(t *testing.T) {
req, _ := http.NewRequest("GET", "/users", nil)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(CreateUserHandler)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
}
---
02.中间件测试
a.中间件概念
中间件是包装HTTP处理器的函数,用于在请求处理前后执行额外逻辑,如认证、日志、CORS等。测试中间件需要验证它正确地修改请求或响应,并调用下一个处理器。
b.中间件测试示例
---
// 中间件测试示例
package middleware
import (
"testing"
"net/http"
"net/http/httptest"
"github.com/stretchr/testify/assert"
)
// ========== 中间件定义 ==========
// 日志中间件
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 记录请求信息
log.Printf("%s %s", r.Method, r.URL.Path)
// 调用下一个处理器
next.ServeHTTP(w, r)
})
}
// 认证中间件
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 检查认证token
token := r.Header.Get("Authorization")
if token == "" {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
if token != "valid-token" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Forbidden"))
return
}
// 认证通过,调用下一个处理器
next.ServeHTTP(w, r)
})
}
// CORS中间件
func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 设置CORS头
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
// 处理预检请求
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
// ========== 中间件测试 ==========
// 测试:日志中间件
func TestLoggingMiddleware(t *testing.T) {
// 创建测试处理器
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
// 应用中间件
wrappedHandler := LoggingMiddleware(handler)
req, _ := http.NewRequest("GET", "/test", nil)
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
// 验证请求仍然正常处理
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "OK", rr.Body.String())
}
// 测试:认证中间件-无token
func TestAuthMiddleware_NoToken(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Protected"))
})
wrappedHandler := AuthMiddleware(handler)
req, _ := http.NewRequest("GET", "/protected", nil)
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
// 验证返回401
assert.Equal(t, http.StatusUnauthorized, rr.Code)
assert.Equal(t, "Unauthorized", rr.Body.String())
}
// 测试:认证中间件-无效token
func TestAuthMiddleware_InvalidToken(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Protected"))
})
wrappedHandler := AuthMiddleware(handler)
req, _ := http.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "invalid-token")
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Equal(t, "Forbidden", rr.Body.String())
}
// 测试:认证中间件-有效token
func TestAuthMiddleware_ValidToken(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Protected Content"))
})
wrappedHandler := AuthMiddleware(handler)
req, _ := http.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "valid-token")
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
// 验证请求通过
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "Protected Content", rr.Body.String())
}
// 测试:CORS中间件
func TestCORSMiddleware(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
wrappedHandler := CORSMiddleware(handler)
req, _ := http.NewRequest("GET", "/api", nil)
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
// 验证CORS头已设置
assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin"))
assert.Equal(t, "GET, POST, PUT, DELETE", rr.Header().Get("Access-Control-Allow-Methods"))
assert.Contains(t, rr.Header().Get("Access-Control-Allow-Headers"), "Authorization")
}
// 测试:CORS预检请求
func TestCORSMiddleware_Preflight(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Should not reach here"))
})
wrappedHandler := CORSMiddleware(handler)
req, _ := http.NewRequest("OPTIONS", "/api", nil)
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
// 验证预检请求返回200且不调用下一个处理器
assert.Equal(t, http.StatusOK, rr.Code)
assert.Empty(t, rr.Body.String())
}
// 测试:多个中间件链
func TestMiddlewareChain(t *testing.T) {
finalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Final"))
})
// 组合多个中间件
handler := LoggingMiddleware(
CORSMiddleware(
AuthMiddleware(finalHandler),
),
)
req, _ := http.NewRequest("GET", "/api", nil)
req.Header.Set("Authorization", "valid-token")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
// 验证所有中间件都生效
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "Final", rr.Body.String())
assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin"))
}
---
03.httptest.Server使用
a.测试服务器
httptest.Server可以启动一个真实的HTTP服务器用于测试,适合集成测试场景。相比ResponseRecorder,Server提供完整的HTTP栈,可以测试完整的请求响应流程。
b.Server测试示例
---
// httptest.Server测试示例
package server
import (
"testing"
"net/http"
"net/http/httptest"
"github.com/stretchr/testify/assert"
"io"
)
func TestWithHTTPServer(t *testing.T) {
// 创建测试服务器
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Test Server"))
}))
defer server.Close()
// 发送真实HTTP请求
resp, err := http.Get(server.URL)
assert.NoError(t, err)
defer resp.Body.Close()
// 验证响应
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
assert.Equal(t, "Test Server", string(body))
}
// 测试:带路由的服务器
func TestServerWithRouter(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Users"))
})
mux.HandleFunc("/posts", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Posts"))
})
server := httptest.NewServer(mux)
defer server.Close()
// 测试/users
resp1, _ := http.Get(server.URL + "/users")
body1, _ := io.ReadAll(resp1.Body)
assert.Equal(t, "Users", string(body1))
resp1.Body.Close()
// 测试/posts
resp2, _ := http.Get(server.URL + "/posts")
body2, _ := io.ReadAll(resp2.Body)
assert.Equal(t, "Posts", string(body2))
resp2.Body.Close()
}
---
04.最佳实践
a.选择合适的测试工具
单元测试HTTP处理器使用httptest.ResponseRecorder,快速且无需网络。集成测试使用httptest.Server,提供完整HTTP栈。端到端测试考虑使用真实服务器或Docker容器。
b.测试组织
为每个HTTP端点编写独立测试,包括正常场景和异常场景。测试不同的HTTP方法、请求头、查询参数、请求体。验证状态码、响应头、响应体的正确性。
2.7 附:包选择建议
01.选择决策树
a.决策流程
选择testify包时应该根据测试场景和需求进行决策。首先确定是否需要断言功能,然后判断断言失败是否应该立即终止测试,接着考虑是否需要mock依赖,最后判断是否需要结构化的测试组织。这个决策流程可以帮助快速选择合适的包组合。
b.决策树图示
---
// testify包选择决策树
package decision
/*
┌─────────────────────────────────────────┐
│ 开始:我需要编写测试 │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 是否需要断言功能? │
├─────────────────────────────────────────┤
│ Yes → 继续 │
│ No → 使用标准库testing包 │
└──────────────┬──────────────────────────┘
│ Yes
▼
┌─────────────────────────────────────────┐
│ 是否是前置条件检查? │
│ (失败应立即终止测试) │
├─────────────────────────────────────────┤
│ Yes → 使用require包 │
│ No → 使用assert包 │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 是否需要隔离外部依赖? │
├─────────────────────────────────────────┤
│ Yes → 添加mock包 │
│ No → 跳过 │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 是否有复杂的初始化和清理? │
│ (多个相关测试需要共享资源) │
├─────────────────────────────────────────┤
│ Yes → 使用suite包 │
│ No → 使用函数式测试 │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 是否测试HTTP处理器? │
├─────────────────────────────────────────┤
│ Yes → 考虑使用http包(可选) │
│ No → 完成 │
└─────────────────────────────────────────┘
========== 典型场景包组合 ==========
场景1:简单单元测试
├─ assert包:验证函数返回值
└─ 推荐指数:★★★★★
场景2:带资源初始化的单元测试
├─ require包:检查资源初始化
├─ assert包:验证业务逻辑
└─ 推荐指数:★★★★★
场景3:需要隔离依赖的单元测试
├─ require包:检查mock对象创建
├─ mock包:模拟外部依赖
├─ assert包:验证业务逻辑
└─ 推荐指数:★★★★★
场景4:数据库集成测试
├─ suite包:管理数据库连接和事务
├─ require包:检查数据库操作
├─ assert包:验证查询结果
└─ 推荐指数:★★★★☆
场景5:HTTP API测试
├─ require包:检查请求创建
├─ assert包:验证响应
├─ http包:简化HTTP测试(可选)
└─ 推荐指数:★★★★☆
场景6:完整的端到端测试
├─ suite包:管理测试环境
├─ require包:检查前置条件
├─ mock包:模拟外部服务
├─ assert包:验证结果
└─ 推荐指数:★★★★★
*/
// 示例:简单单元测试(只用assert)
func TestSimpleUnit(t *testing.T) {
a := assert.New(t)
result := Add(2, 3)
a.Equal(5, result)
}
// 示例:带资源初始化(require + assert)
func TestWithResource(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// require:前置条件
db := ConnectDB()
r.NotNil(db)
defer db.Close()
// assert:业务逻辑
users, err := db.QueryUsers()
a.NoError(err)
a.Len(users, 3)
}
// 示例:隔离依赖(require + mock + assert)
func TestWithMock(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// mock:模拟依赖
mockRepo := new(MockRepository)
mockRepo.On("Get", 1).Return("data", nil)
// require:创建服务
service := NewService(mockRepo)
r.NotNil(service)
// assert:验证行为
result, err := service.Process(1)
a.NoError(err)
a.Equal("data", result)
mockRepo.AssertExpectations(t)
}
func Add(a, b int) int { return a + b }
type DB struct{}
func ConnectDB() *DB { return &DB{} }
func (db *DB) Close() {}
func (db *DB) QueryUsers() ([]string, error) { return []string{"a", "b", "c"}, nil }
type MockRepository struct{ mock.Mock }
func (m *MockRepository) Get(id int) (string, error) {
args := m.Called(id)
return args.String(0), args.Error(1)
}
type Service struct{}
func NewService(repo *MockRepository) *Service { return &Service{} }
func (s *Service) Process(id int) (string, error) { return "data", nil }
---
02.常见场景建议
a.纯逻辑函数测试
对于不依赖外部资源的纯函数,使用assert包即可。编写简单直接的测试,验证输入输出关系。不需要mock和suite,保持测试简单。适合工具函数、算法实现、数据转换等场景。
b.业务逻辑层测试
业务逻辑层通常依赖数据访问层和外部服务,需要使用mock隔离依赖。使用require检查mock对象创建,使用assert验证业务逻辑,使用mock包模拟依赖。这是最常见的测试场景。
c.数据访问层测试
数据访问层测试需要真实数据库连接,使用suite管理数据库连接和事务。在SetupSuite中连接数据库,在SetupTest中准备测试数据,在TearDownTest中清理数据。使用require检查数据库操作,使用assert验证查询结果。
d.场景示例
---
// 常见场景包选择示例
package scenarios
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// ========== 场景1:纯逻辑函数(只用assert)==========
func TestPureFunction(t *testing.T) {
a := assert.New(t)
// 测试字符串处理函数
a.Equal("HELLO", ToUpper("hello"))
a.Equal("hello", ToLower("HELLO"))
// 测试数学计算函数
a.Equal(15, Sum([]int{1, 2, 3, 4, 5}))
a.Equal(3.0, Average([]float64{1, 2, 3, 4, 5}))
// 测试数据验证函数
a.True(IsValidEmail("[email protected]"))
a.False(IsValidEmail("invalid"))
}
// ========== 场景2:业务逻辑层(require + mock + assert)==========
func TestBusinessLogic(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// Mock依赖
mockUserRepo := new(MockUserRepository)
mockOrderRepo := new(MockOrderRepository)
mockNotifier := new(MockNotifier)
// 设置期望
user := &User{ID: 1, Name: "Alice", Email: "[email protected]"}
mockUserRepo.On("FindByID", 1).Return(user, nil)
mockOrderRepo.On("Create", mock.Anything).Return(nil)
mockNotifier.On("Send", user.Email, mock.Anything).Return(nil)
// 创建服务(使用require确保成功)
service := NewOrderService(mockUserRepo, mockOrderRepo, mockNotifier)
r.NotNil(service)
// 测试业务逻辑(使用assert)
order, err := service.CreateOrder(1, []OrderItem{{ProductID: 1, Quantity: 2}})
a.NoError(err)
a.NotNil(order)
a.Equal(1, order.UserID)
// 验证mock调用
mockUserRepo.AssertExpectations(t)
mockOrderRepo.AssertExpectations(t)
mockNotifier.AssertExpectations(t)
}
// ========== 场景3:数据访问层(suite + require + assert)==========
type RepositoryTestSuite struct {
suite.Suite
db *sql.DB
repo *UserRepository
}
func (s *RepositoryTestSuite) SetupSuite() {
var err error
s.db, err = sql.Open("postgres", "postgres://localhost/testdb")
s.Require().NoError(err)
}
func (s *RepositoryTestSuite) SetupTest() {
s.db.Exec("TRUNCATE TABLE users")
s.db.Exec("INSERT INTO users (name, email) VALUES ('Alice', '[email protected]')")
s.repo = NewUserRepository(s.db)
}
func (s *RepositoryTestSuite) TestFindByID() {
user, err := s.repo.FindByID(1)
s.NoError(err)
s.NotNil(user)
s.Equal("Alice", user.Name)
}
func (s *RepositoryTestSuite) TestCreate() {
newUser := &User{Name: "Bob", Email: "[email protected]"}
err := s.repo.Create(newUser)
s.NoError(err)
s.NotZero(newUser.ID)
}
func (s *RepositoryTestSuite) TearDownSuite() {
if s.db != nil {
s.db.Close()
}
}
func TestRepositoryTestSuite(t *testing.T) {
suite.Run(t, new(RepositoryTestSuite))
}
// ========== 场景4:HTTP API(require + assert)==========
func TestHTTPAPI(t *testing.T) {
r := require.New(t)
a := assert.New(t)
// 创建请求
requestBody := `{"name":"Alice","email":"[email protected]"}`
req, err := http.NewRequest("POST", "/users", strings.NewReader(requestBody))
r.NoError(err)
req.Header.Set("Content-Type", "application/json")
// 执行请求
rr := httptest.NewRecorder()
handler := UserHandler()
handler.ServeHTTP(rr, req)
// 验证响应
a.Equal(http.StatusCreated, rr.Code)
a.Contains(rr.Header().Get("Content-Type"), "application/json")
var response map[string]interface{}
json.Unmarshal(rr.Body.Bytes(), &response)
a.Equal("Alice", response["name"])
}
// 辅助函数和类型
func ToUpper(s string) string { return strings.ToUpper(s) }
func ToLower(s string) string { return strings.ToLower(s) }
func Sum(nums []int) int {
sum := 0
for _, n := range nums {
sum += n
}
return sum
}
func Average(nums []float64) float64 {
if len(nums) == 0 {
return 0
}
sum := 0.0
for _, n := range nums {
sum += n
}
return sum / float64(len(nums))
}
func IsValidEmail(email string) bool { return strings.Contains(email, "@") }
type User struct {
ID int
Name string
Email string
}
type OrderItem struct {
ProductID int
Quantity int
}
type Order struct {
UserID int
Items []OrderItem
}
type MockUserRepository struct{ mock.Mock }
func (m *MockUserRepository) FindByID(id int) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
type MockOrderRepository struct{ mock.Mock }
func (m *MockOrderRepository) Create(order *Order) error {
args := m.Called(order)
return args.Error(0)
}
type MockNotifier struct{ mock.Mock }
func (m *MockNotifier) Send(email, message string) error {
args := m.Called(email, message)
return args.Error(0)
}
type OrderService struct{}
func NewOrderService(userRepo *MockUserRepository, orderRepo *MockOrderRepository, notifier *MockNotifier) *OrderService {
return &OrderService{}
}
func (s *OrderService) CreateOrder(userID int, items []OrderItem) (*Order, error) {
return &Order{UserID: userID, Items: items}, nil
}
type UserRepository struct{ db *sql.DB }
func NewUserRepository(db *sql.DB) *UserRepository { return &UserRepository{db: db} }
func (r *UserRepository) FindByID(id int) (*User, error) { return &User{ID: id, Name: "Alice", Email: "[email protected]"}, nil }
func (r *UserRepository) Create(user *User) error { user.ID = 1; return nil }
func UserHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"name":"Alice"}`))
})
}
---
03.性能考虑
a.包开销
assert和require包由于使用反射进行深度比较,有一定性能开销,但在测试场景下完全可以接受。mock包维护期望列表和调用记录,有少量内存开销。suite包使用反射扫描测试方法,启动略慢但执行性能正常。
b.优化建议
避免在测试中进行不必要的复杂断言,简单的相等检查即可。合理使用suite的SetupSuite和SetupTest,避免重复初始化。对于性能测试基准测试,考虑使用标准库而非testify以减少干扰。
c.性能对比
---
// 性能对比基准测试
package performance
import (
"testing"
"github.com/stretchr/testify/assert"
)
// 标准库断言
func BenchmarkStandardAssertion(b *testing.B) {
for i := 0; i < b.N; i++ {
result := Add(2, 3)
if result != 5 {
b.Errorf("expected 5, got %d", result)
}
}
}
// testify断言
func BenchmarkTestifyAssertion(b *testing.B) {
for i := 0; i < b.N; i++ {
result := Add(2, 3)
assert.Equal(b, 5, result)
}
}
/*
结果参考:
BenchmarkStandardAssertion-8 100000000 12 ns/op
BenchmarkTestifyAssertion-8 20000000 85 ns/op
结论:testify约慢7倍,但绝对时间仍在纳秒级
对于测试场景,这个开销完全可以接受
*/
func Add(a, b int) int { return a + b }
---
04.团队协作
a.统一规范
团队应该建立统一的testify使用规范,明确assert vs require的使用场景。统一mock对象的命名和组织方式。统一suite的生命周期钩子使用模式。代码审查时检查测试代码的规范性。
b.最佳实践
在项目README中说明测试规范,提供测试示例代码。新成员入职时培训testify使用方法。建立测试模板,降低编写测试的门槛。定期Review测试代码质量,持续改进。
c.规范示例
---
// 团队测试规范示例
package standards
/*
========== 项目测试规范 ==========
1. 包选择规则
- 前置条件:require包
- 多个检查:assert包
- 外部依赖:mock包
- 复杂初始化:suite包
2. 命名规范
- Mock对象:Mock + 接口名(MockUserRepository)
- 测试套件:类名 + TestSuite(UserServiceTestSuite)
- 测试函数:Test + 功能描述(TestCreateUser)
- 子测试:使用Run + 描述性名称
3. 文件组织
- 测试文件与源文件同目录
- 命名:源文件_test.go
- Mock对象可以单独放在mocks目录
4. 测试结构
- 使用AAA模式:Arrange-Act-Assert
- 每个测试只测试一个行为
- 测试名称清晰描述测试内容
5. 断言风格
- 优先使用New(t)创建断言对象
- 断言失败提供有意义的消息
- 相关断言组织在一起
6. Mock使用
- Mock对象在测试函数内创建
- 设置期望后立即使用
- 测试结束前验证期望
7. Suite使用
- 合理使用生命周期钩子
- 确保测试隔离
- 避免测试间共享可变状态
========== 代码示例 ==========
*/
// 标准测试函数结构
func TestStandardStructure(t *testing.T) {
// Arrange:准备测试数据和mock
r := require.New(t)
a := assert.New(t)
mockRepo := new(MockRepository)
mockRepo.On("Get", 1).Return("data", nil)
service := NewService(mockRepo)
r.NotNil(service)
// Act:执行被测方法
result, err := service.Process(1)
// Assert:验证结果
a.NoError(err)
a.Equal("data", result)
mockRepo.AssertExpectations(t)
}
// 使用子测试组织多个场景
func TestWithSubtests(t *testing.T) {
t.Run("成功场景", func(t *testing.T) {
// 测试代码
})
t.Run("错误场景", func(t *testing.T) {
// 测试代码
})
t.Run("边界场景", func(t *testing.T) {
// 测试代码
})
}
type MockRepository struct{ mock.Mock }
func (m *MockRepository) Get(id int) (string, error) {
args := m.Called(id)
return args.String(0), args.Error(1)
}
type Service struct{}
func NewService(repo *MockRepository) *Service { return &Service{} }
func (s *Service) Process(id int) (string, error) { return "data", nil }
---
3 断言系统详解
3.1 基础断言
01.Equal断言
a.功能说明
Equal是testify中最常用的断言方法,用于检查两个值是否深度相等。它使用reflect.DeepEqual进行比较,支持所有Go类型包括基本类型、结构体、切片、map、指针等。Equal不仅比较值,还递归比较嵌套的数据结构,确保完全一致。
b.使用场景
Equal适用于几乎所有的相等性检查场景。基本类型比较如整数、字符串、布尔值,复杂类型比较如结构体、切片、map,以及指针指向的值比较。Equal是编写测试时的首选断言方法。
c.代码示例
---
// Equal断言详解
package equal
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEqualBasicTypes(t *testing.T) {
a := assert.New(t)
// 整数比较
a.Equal(42, 40+2)
a.Equal(int32(100), int32(100))
a.Equal(int64(1000), int64(1000))
// 浮点数比较(注意精度问题)
a.Equal(3.14, 3.14)
a.Equal(float32(2.5), float32(2.5))
// 字符串比较
a.Equal("hello", "hel"+"lo")
a.Equal("", "")
// 布尔值比较
a.Equal(true, true)
a.Equal(false, !true)
// 字节切片比较
a.Equal([]byte("hello"), []byte("hello"))
}
func TestEqualStructs(t *testing.T) {
a := assert.New(t)
type Person struct {
Name string
Age int
}
// 结构体比较
p1 := Person{Name: "Alice", Age: 25}
p2 := Person{Name: "Alice", Age: 25}
a.Equal(p1, p2)
// 结构体指针比较(比较值,不是指针地址)
a.Equal(&p1, &p2)
// 嵌套结构体比较
type Address struct {
City string
Country string
}
type Employee struct {
Person Person
Address Address
Salary float64
}
e1 := Employee{
Person: Person{Name: "Bob", Age: 30},
Address: Address{City: "Beijing", Country: "China"},
Salary: 50000.0,
}
e2 := Employee{
Person: Person{Name: "Bob", Age: 30},
Address: Address{City: "Beijing", Country: "China"},
Salary: 50000.0,
}
a.Equal(e1, e2)
}
func TestEqualSlices(t *testing.T) {
a := assert.New(t)
// 切片比较
a.Equal([]int{1, 2, 3}, []int{1, 2, 3})
a.Equal([]string{"a", "b", "c"}, []string{"a", "b", "c"})
// 空切片vs nil切片(不相等)
var nilSlice []int
emptySlice := []int{}
// a.Equal(nilSlice, emptySlice) // 失败:nil != []
a.Nil(nilSlice)
a.NotNil(emptySlice)
// 嵌套切片
nested := [][]int{{1, 2}, {3, 4}, {5, 6}}
expected := [][]int{{1, 2}, {3, 4}, {5, 6}}
a.Equal(expected, nested)
// 结构体切片
type User struct {
ID int
Name string
}
users1 := []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}
users2 := []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}
a.Equal(users1, users2)
}
func TestEqualMaps(t *testing.T) {
a := assert.New(t)
// map比较
m1 := map[string]int{"a": 1, "b": 2, "c": 3}
m2 := map[string]int{"a": 1, "b": 2, "c": 3}
a.Equal(m1, m2)
// map顺序无关
m3 := map[string]int{"c": 3, "a": 1, "b": 2}
a.Equal(m1, m3)
// 嵌套map
nested1 := map[string]map[string]int{
"group1": {"a": 1, "b": 2},
"group2": {"c": 3, "d": 4},
}
nested2 := map[string]map[string]int{
"group1": {"a": 1, "b": 2},
"group2": {"c": 3, "d": 4},
}
a.Equal(nested1, nested2)
// map值为结构体
type Config struct {
Host string
Port int
}
configs1 := map[string]Config{
"db": {Host: "localhost", Port: 3306},
"redis": {Host: "localhost", Port: 6379},
}
configs2 := map[string]Config{
"db": {Host: "localhost", Port: 3306},
"redis": {Host: "localhost", Port: 6379},
}
a.Equal(configs1, configs2)
}
func TestEqualPointers(t *testing.T) {
a := assert.New(t)
// 指针比较(比较指向的值)
x := 42
y := 42
a.Equal(&x, &y) // 通过:值相等
// 指针指向相同对象
p1 := &x
p2 := &x
a.Same(p1, p2) // 使用Same检查指针相同
// nil指针
var p3 *int
var p4 *int
a.Equal(p3, p4) // 都是nil
}
---
02.NotEqual断言
a.功能说明
NotEqual断言用于检查两个值不相等,是Equal的反向断言。在测试负面场景时非常有用,如验证两个对象确实不同,或验证修改操作确实改变了值。NotEqual同样使用深度比较,确保值在各个层级都不同。
b.使用场景
NotEqual适用于验证值确实发生了变化,测试互斥条件,验证不同的输入产生不同的输出。在测试数据修改、状态变更、配置差异等场景中特别有用。
c.代码示例
---
// NotEqual断言详解
package notequal
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNotEqualBasics(t *testing.T) {
a := assert.New(t)
// 基本类型不相等
a.NotEqual(1, 2)
a.NotEqual("hello", "world")
a.NotEqual(true, false)
a.NotEqual(3.14, 2.71)
// 不同类型(即使值看起来相同)
a.NotEqual(int32(5), int64(5))
a.NotEqual(float32(1.0), float64(1.0))
}
func TestNotEqualAfterModification(t *testing.T) {
a := assert.New(t)
type Counter struct {
Value int
}
counter := Counter{Value: 0}
before := counter
// 修改后应该不相等
counter.Value = 10
a.NotEqual(before, counter)
// 验证确实改变了
a.Equal(0, before.Value)
a.Equal(10, counter.Value)
}
func TestNotEqualSlices(t *testing.T) {
a := assert.New(t)
// 不同长度
a.NotEqual([]int{1, 2, 3}, []int{1, 2})
// 相同长度不同元素
a.NotEqual([]int{1, 2, 3}, []int{1, 2, 4})
// 不同顺序
a.NotEqual([]int{1, 2, 3}, []int{3, 2, 1})
}
func TestNotEqualMaps(t *testing.T) {
a := assert.New(t)
m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"a": 1, "b": 3} // b的值不同
m3 := map[string]int{"a": 1} // 缺少b
a.NotEqual(m1, m2)
a.NotEqual(m1, m3)
}
---
03.Nil和NotNil断言
a.功能说明
Nil断言检查值是否为nil,NotNil检查值不为nil。在Go语言中,可以为nil的类型包括指针、接口、切片、map、channel和函数。Nil断言是测试中最常用的存在性检查方法,特别是在检查函数返回值时。
b.nil类型说明
Go语言中nil的含义因类型而异。对于指针,nil表示不指向任何对象。对于接口,nil表示没有存储任何值。对于切片和map,nil表示未初始化。对于channel,nil表示未创建。理解nil的语义对正确使用Nil断言很重要。
c.代码示例
---
// Nil和NotNil断言详解
package nil
import (
"testing"
"github.com/stretchr/testify/assert"
"errors"
)
func TestNilPointers(t *testing.T) {
a := assert.New(t)
// nil指针
var ptr *int
a.Nil(ptr)
// 非nil指针
x := 42
a.NotNil(&x)
// 结构体指针
type User struct {
Name string
}
var user *User
a.Nil(user)
user = &User{Name: "Alice"}
a.NotNil(user)
}
func TestNilInterfaces(t *testing.T) {
a := assert.New(t)
// nil error接口
var err error
a.Nil(err)
// 非nil error
err = errors.New("something went wrong")
a.NotNil(err)
// nil接口值
var reader io.Reader
a.Nil(reader)
// 非nil接口值
reader = strings.NewReader("hello")
a.NotNil(reader)
}
func TestNilSlices(t *testing.T) {
a := assert.New(t)
// nil切片
var slice []int
a.Nil(slice)
// 空切片(不是nil)
emptySlice := []int{}
a.NotNil(emptySlice)
// make创建的切片
madeSlice := make([]int, 0)
a.NotNil(madeSlice)
// nil切片和空切片的区别
a.Len(slice, 0) // nil切片长度为0
a.Len(emptySlice, 0) // 空切片长度也为0
// 但它们不相等
a.NotEqual(slice, emptySlice)
}
func TestNilMaps(t *testing.T) {
a := assert.New(t)
// nil map
var m map[string]int
a.Nil(m)
// 空map(不是nil)
emptyMap := map[string]int{}
a.NotNil(emptyMap)
// make创建的map
madeMap := make(map[string]int)
a.NotNil(madeMap)
// nil map不能写入(会panic)
// m["key"] = 1 // panic!
// 空map可以写入
emptyMap["key"] = 1
a.Equal(1, emptyMap["key"])
}
func TestNilChannels(t *testing.T) {
a := assert.New(t)
// nil channel
var ch chan int
a.Nil(ch)
// 创建的channel
ch = make(chan int)
a.NotNil(ch)
defer close(ch)
}
func TestNilFunctions(t *testing.T) {
a := assert.New(t)
// nil函数
var fn func()
a.Nil(fn)
// 非nil函数
fn = func() {}
a.NotNil(fn)
}
func TestNilInReturnValues(t *testing.T) {
a := assert.New(t)
// 测试返回nil的函数
user, err := FindUser(999)
a.Nil(user)
a.NotNil(err)
// 测试返回非nil的函数
user, err = FindUser(1)
a.NotNil(user)
a.Nil(err)
}
type User struct {
ID int
Name string
}
func FindUser(id int) (*User, error) {
if id == 999 {
return nil, errors.New("user not found")
}
return &User{ID: id, Name: "Alice"}, nil
}
---
04.True和False断言
a.功能说明
True断言检查布尔表达式是否为true,False检查是否为false。虽然看似简单,但在测试复杂条件、验证状态、检查标志位等场景中非常实用。True和False让测试代码更加语义化和易读。
b.使用技巧
True和False适合测试返回布尔值的函数,验证复杂的条件表达式,检查对象的状态标志。相比使用Equal(true, value),使用True(value)更加简洁和直观。可以配合自定义错误消息,清楚说明被测试的条件。
c.代码示例
---
// True和False断言详解
package boolean
import (
"testing"
"github.com/stretchr/testify/assert"
"strings"
"regexp"
)
func TestTrueBasics(t *testing.T) {
a := assert.New(t)
// 直接的布尔值
a.True(true)
a.False(false)
// 布尔表达式
a.True(5 > 3)
a.False(5 < 3)
a.True(10 == 10)
a.False(10 != 10)
}
func TestTrueFunctions(t *testing.T) {
a := assert.New(t)
// 测试返回bool的函数
a.True(IsValidEmail("[email protected]"))
a.False(IsValidEmail("invalid"))
a.True(IsPrime(7))
a.False(IsPrime(8))
a.True(IsEmpty(""))
a.False(IsEmpty("hello"))
}
func TestTrueConditions(t *testing.T) {
a := assert.New(t)
user := User{
Name: "Alice",
Age: 25,
Active: true,
Verified: true,
}
// 测试对象状态
a.True(user.Active, "用户应该是激活状态")
a.True(user.Verified, "用户应该已验证")
a.True(user.Age >= 18, "用户应该是成年人")
a.True(len(user.Name) > 0, "用户名不应为空")
// 组合条件
a.True(user.Active && user.Verified, "用户应该激活且已验证")
a.True(user.Age >= 18 && user.Age < 65, "用户年龄应在18-65之间")
}
func TestTrueStringOperations(t *testing.T) {
a := assert.New(t)
text := "Hello, World!"
// 字符串检查
a.True(strings.Contains(text, "World"))
a.True(strings.HasPrefix(text, "Hello"))
a.True(strings.HasSuffix(text, "!"))
a.True(len(text) > 10)
// 正则匹配
matched, _ := regexp.MatchString(`^Hello`, text)
a.True(matched)
}
func TestTrueCollections(t *testing.T) {
a := assert.New(t)
numbers := []int{1, 2, 3, 4, 5}
// 集合检查
a.True(len(numbers) == 5)
a.True(len(numbers) > 0)
a.True(contains(numbers, 3))
a.False(contains(numbers, 10))
// map检查
config := map[string]string{
"host": "localhost",
"port": "8080",
}
_, exists := config["host"]
a.True(exists, "配置应该包含host")
_, exists = config["password"]
a.False(exists, "配置不应该包含password")
}
func TestTrueCustomValidation(t *testing.T) {
a := assert.New(t)
// 自定义验证逻辑
password := "MyP@ssw0rd"
a.True(len(password) >= 8, "密码长度应该>=8")
a.True(containsUpperCase(password), "密码应包含大写字母")
a.True(containsLowerCase(password), "密码应包含小写字母")
a.True(containsDigit(password), "密码应包含数字")
a.True(containsSpecialChar(password), "密码应包含特殊字符")
}
type User struct {
Name string
Age int
Active bool
Verified bool
}
func IsValidEmail(email string) bool {
return strings.Contains(email, "@") && strings.Contains(email, ".")
}
func IsPrime(n int) bool {
if n < 2 {
return false
}
for i := 2; i*i <= n; i++ {
if n%i == 0 {
return false
}
}
return true
}
func IsEmpty(s string) bool {
return len(s) == 0
}
func contains(slice []int, value int) bool {
for _, v := range slice {
if v == value {
return true
}
}
return false
}
func containsUpperCase(s string) bool {
for _, c := range s {
if c >= 'A' && c <= 'Z' {
return true
}
}
return false
}
func containsLowerCase(s string) bool {
for _, c := range s {
if c >= 'a' && c <= 'z' {
return true
}
}
return false
}
func containsDigit(s string) bool {
for _, c := range s {
if c >= '0' && c <= '9' {
return true
}
}
return false
}
func containsSpecialChar(s string) bool {
return strings.ContainsAny(s, "!@#$%^&*()_+-=[]{}|;:,.<>?")
}
---
3.2 比较断言
01.数值比较断言
a.Greater和Less系列
testify提供完整的数值比较断言方法。Greater检查值大于,GreaterOrEqual检查大于等于,Less检查小于,LessOrEqual检查小于等于。这些方法支持所有可比较的数值类型,让数值范围验证变得简单直观。
b.Positive和Negative
Positive断言检查数值是否为正数,Negative检查是否为负数。这两个方法是Greater和Less的特殊形式,用于检查数值的符号,使测试代码更加语义化。
c.代码示例
---
// 数值比较断言详解
package comparison
import (
"testing"
"github.com/stretchr/testify/assert"
"time"
)
func TestGreaterAndLess(t *testing.T) {
a := assert.New(t)
// Greater:大于
a.Greater(10, 5)
a.Greater(3.14, 2.71)
a.Greater(int64(1000), int64(999))
// GreaterOrEqual:大于等于
a.GreaterOrEqual(10, 10)
a.GreaterOrEqual(10, 5)
a.GreaterOrEqual(100.0, 100.0)
// Less:小于
a.Less(5, 10)
a.Less(-1, 0)
a.Less(1.5, 2.5)
// LessOrEqual:小于等于
a.LessOrEqual(5, 5)
a.LessOrEqual(5, 10)
a.LessOrEqual(-10.0, -10.0)
}
func TestPositiveAndNegative(t *testing.T) {
a := assert.New(t)
// Positive:正数
a.Positive(1)
a.Positive(42)
a.Positive(3.14)
a.Positive(int64(1000))
// Negative:负数
a.Negative(-1)
a.Negative(-42)
a.Negative(-3.14)
a.Negative(int64(-1000))
// 零不是正数也不是负数
// a.Positive(0) // 失败
// a.Negative(0) // 失败
}
func TestComparisonUseCases(t *testing.T) {
a := assert.New(t)
// 场景1:年龄验证
age := 25
a.GreaterOrEqual(age, 18, "必须是成年人")
a.Less(age, 65, "必须是工作年龄")
// 场景2:分数验证
score := 85.5
a.GreaterOrEqual(score, 0.0, "分数不能为负")
a.LessOrEqual(score, 100.0, "分数不能超过100")
a.GreaterOrEqual(score, 60.0, "分数及格")
// 场景3:价格验证
price := 99.99
a.Positive(price, "价格必须为正")
a.Less(price, 1000.0, "价格合理")
// 场景4:余额验证
balance := -50.0
a.Negative(balance, "账户透支")
// 场景5:数组长度验证
items := []int{1, 2, 3, 4, 5}
a.Greater(len(items), 0, "列表不能为空")
a.LessOrEqual(len(items), 100, "列表不能太长")
}
func TestComparisonWithDifferentTypes(t *testing.T) {
a := assert.New(t)
// int类型
var i int = 10
a.Greater(i, 5)
// int32类型
var i32 int32 = 10
a.Greater(i32, int32(5))
// int64类型
var i64 int64 = 10
a.Greater(i64, int64(5))
// float32类型
var f32 float32 = 10.5
a.Greater(f32, float32(5.5))
// float64类型
var f64 float64 = 10.5
a.Greater(f64, 5.5)
// uint类型
var u uint = 10
a.Greater(u, uint(5))
}
func TestComparisonWithTime(t *testing.T) {
a := assert.New(t)
now := time.Now()
past := now.Add(-1 * time.Hour)
future := now.Add(1 * time.Hour)
// 时间比较(Time实现了comparable接口)
a.True(future.After(now))
a.True(past.Before(now))
// 使用Greater比较Unix时间戳
a.Greater(future.Unix(), now.Unix())
a.Less(past.Unix(), now.Unix())
}
---
02.浮点数比较断言
a.InDelta系列
由于浮点数精度问题,直接使用Equal比较浮点数可能失败。InDelta允许指定误差范围,只要两个浮点数的差值在delta范围内就认为相等。InDeltaSlice和InDeltaMapValues分别用于比较浮点数切片和map。
b.InEpsilon相对误差
InEpsilon使用相对误差而非绝对误差进行比较,适合比较不同数量级的浮点数。epsilon表示相对误差的百分比,如0.01表示允许1%的误差。
c.代码示例
---
// 浮点数比较断言详解
package float
import (
"testing"
"github.com/stretchr/testify/assert"
"math"
)
func TestInDelta(t *testing.T) {
a := assert.New(t)
// InDelta:绝对误差比较
// 0.1 + 0.2 在浮点数运算中不完全等于0.3
result := 0.1 + 0.2
a.InDelta(0.3, result, 0.0001, "允许0.0001的误差")
// 直接Equal会失败
// a.Equal(0.3, result) // 可能失败
// 更多示例
a.InDelta(3.14159, math.Pi, 0.00001)
a.InDelta(2.71828, math.E, 0.00001)
// 较大的数值
a.InDelta(1000000.0, 1000000.5, 1.0)
}
func TestInDeltaSlice(t *testing.T) {
a := assert.New(t)
// InDeltaSlice:切片中每个元素都允许误差
expected := []float64{1.0, 2.0, 3.0}
actual := []float64{1.0001, 1.9999, 3.0001}
a.InDeltaSlice(expected, actual, 0.001)
// 科学计算结果比较
calculated := []float64{
math.Sqrt(2),
math.Sqrt(3),
math.Sqrt(5),
}
expected2 := []float64{
1.414213562373095,
1.732050807568877,
2.236067977499790,
}
a.InDeltaSlice(expected2, calculated, 0.000000000000001)
}
func TestInDeltaMapValues(t *testing.T) {
a := assert.New(t)
// InDeltaMapValues:map中每个值都允许误差
expected := map[string]float64{
"pi": 3.14159,
"e": 2.71828,
}
actual := map[string]float64{
"pi": 3.14160,
"e": 2.71829,
}
a.InDeltaMapValues(expected, actual, 0.00001)
}
func TestInEpsilon(t *testing.T) {
a := assert.New(t)
// InEpsilon:相对误差比较
// 允许1%的相对误差
a.InEpsilon(1000, 1010, 0.01)
a.InEpsilon(100, 101, 0.01)
// 不同数量级的数值
a.InEpsilon(1000000, 1010000, 0.01) // 1%误差
a.InEpsilon(0.001, 0.00101, 0.01) // 1%误差
// 金融计算
price := 99.99
discountedPrice := price * 0.9
a.InEpsilon(89.991, discountedPrice, 0.001)
}
func TestInEpsilonSlice(t *testing.T) {
a := assert.New(t)
// InEpsilonSlice:切片相对误差比较
expected := []float64{100, 200, 300}
actual := []float64{101, 202, 303}
a.InEpsilonSlice(expected, actual, 0.02) // 允许2%误差
}
func TestFloatComparisonScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:物理计算
g := 9.8 // 重力加速度
calculated := 9.81
a.InDelta(g, calculated, 0.1)
// 场景2:统计计算
mean := 10.5
variance := 2.3
stddev := math.Sqrt(variance)
expectedStddev := 1.5165750888103102
a.InDelta(expectedStddev, stddev, 0.0001)
// 场景3:金融计算
principal := 10000.0
rate := 0.05
years := 10.0
amount := principal * math.Pow(1+rate, years)
expectedAmount := 16288.95
a.InDelta(expectedAmount, amount, 1.0)
// 场景4:机器学习(损失函数)
loss := 0.0234567
targetLoss := 0.0234000
a.InDelta(targetLoss, loss, 0.0001)
}
func TestFloatEdgeCases(t *testing.T) {
a := assert.New(t)
// 零值比较
a.InDelta(0.0, 0.0, 0.0001)
a.InDelta(0.0, 0.00001, 0.0001)
// 负数比较
a.InDelta(-3.14, -3.141, 0.01)
// 非常小的数
a.InDelta(1e-10, 1.1e-10, 1e-11)
// 非常大的数
a.InDelta(1e10, 1.0000001e10, 1e3)
// 无穷大
a.Equal(math.Inf(1), math.Inf(1))
a.NotEqual(math.Inf(1), math.Inf(-1))
// NaN(不能用Equal比较)
a.True(math.IsNaN(math.NaN()))
}
---
03.字符串比较断言
a.Contains和NotContains
Contains检查字符串是否包含子串,NotContains检查不包含。这两个方法也适用于切片和map,用于检查集合是否包含特定元素。Contains是测试中非常常用的断言方法。
b.Regexp和NotRegexp
Regexp使用正则表达式匹配字符串,NotRegexp检查不匹配。适合验证字符串格式如邮箱、电话号码、URL等。正则表达式可以是字符串或编译好的regexp.Regexp对象。
c.代码示例
---
// 字符串比较断言详解
package string
import (
"testing"
"github.com/stretchr/testify/assert"
"regexp"
)
func TestContains(t *testing.T) {
a := assert.New(t)
// 字符串包含
text := "Hello, World!"
a.Contains(text, "World")
a.Contains(text, "Hello")
a.Contains(text, ",")
a.NotContains(text, "Goodbye")
// 大小写敏感
a.Contains("Hello", "Hell")
a.NotContains("Hello", "hell") // 小写
// 空字符串
a.Contains("hello", "") // 任何字符串都包含空字符串
// 错误消息中的关键字
errMsg := "failed to connect to database: connection timeout"
a.Contains(errMsg, "database")
a.Contains(errMsg, "timeout")
a.NotContains(errMsg, "success")
}
func TestRegexp(t *testing.T) {
a := assert.New(t)
// 邮箱验证
email := "[email protected]"
emailPattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
a.Regexp(emailPattern, email)
invalidEmail := "invalid-email"
a.NotRegexp(emailPattern, invalidEmail)
// 电话号码验证
phone := "13812345678"
phonePattern := `^1[3-9]\d{9}$`
a.Regexp(phonePattern, phone)
// URL验证
url := "https://www.example.com/path?query=value"
urlPattern := `^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`
a.Regexp(urlPattern, url)
// 日期格式验证
date := "2024-01-15"
datePattern := `^\d{4}-\d{2}-\d{2}$`
a.Regexp(datePattern, date)
// IP地址验证
ip := "192.168.1.1"
ipPattern := `^(\d{1,3}\.){3}\d{1,3}$`
a.Regexp(ipPattern, ip)
}
func TestRegexpWithCompiledPattern(t *testing.T) {
a := assert.New(t)
// 预编译的正则表达式
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
a.Regexp(emailRegex, "[email protected]")
a.NotRegexp(emailRegex, "invalid")
}
func TestStringComparisonScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:日志消息验证
logMsg := "[INFO] 2024-01-15 10:30:45 User logged in successfully"
a.Contains(logMsg, "[INFO]")
a.Contains(logMsg, "User logged in")
a.Regexp(`\d{4}-\d{2}-\d{2}`, logMsg)
// 场景2:API响应验证
apiResponse := `{"status":"success","data":{"id":123,"name":"Alice"}}`
a.Contains(apiResponse, "success")
a.Contains(apiResponse, "Alice")
a.Regexp(`"id":\d+`, apiResponse)
// 场景3:文件路径验证
filepath := "/home/user/documents/file.txt"
a.Contains(filepath, "/documents/")
a.Contains(filepath, "file.txt")
a.Regexp(`\.txt$`, filepath)
// 场景4:版本号验证
version := "v1.2.3"
a.Regexp(`^v\d+\.\d+\.\d+$`, version)
// 场景5:SQL查询验证
query := "SELECT * FROM users WHERE age > 18 ORDER BY name"
a.Contains(query, "SELECT")
a.Contains(query, "FROM users")
a.Regexp(`WHERE\s+\w+\s*[><=]`, query)
}
func TestHasPrefixAndSuffix(t *testing.T) {
a := assert.New(t)
text := "prefix_content_suffix"
// 使用Contains模拟HasPrefix
a.True(strings.HasPrefix(text, "prefix_"))
a.False(strings.HasPrefix(text, "suffix"))
// 使用Contains模拟HasSuffix
a.True(strings.HasSuffix(text, "_suffix"))
a.False(strings.HasSuffix(text, "prefix"))
// 或使用Regexp
a.Regexp(`^prefix_`, text)
a.Regexp(`_suffix$`, text)
}
---
04.Same断言
a.功能说明
Same断言检查两个指针是否指向同一个对象,即内存地址相同。与Equal不同,Same不比较值,只比较指针地址。Same适合验证对象引用、单例模式、缓存命中等场景。
b.Same vs Equal
Equal比较值是否相等,即使是不同对象只要值相同就通过。Same比较指针地址,只有指向同一对象才通过。理解两者的区别对正确使用断言很重要。
c.代码示例
---
// Same断言详解
package same
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSameBasics(t *testing.T) {
a := assert.New(t)
// 指向同一对象
obj := &User{Name: "Alice"}
ref1 := obj
ref2 := obj
a.Same(ref1, ref2) // 通过:指向同一对象
a.Equal(ref1, ref2) // 也通过:值相等
// 不同对象但值相等
obj1 := &User{Name: "Alice"}
obj2 := &User{Name: "Alice"}
a.NotSame(obj1, obj2) // 通过:不是同一对象
a.Equal(obj1, obj2) // 通过:值相等
}
func TestSameSingleton(t *testing.T) {
a := assert.New(t)
// 测试单例模式
config1 := GetConfig()
config2 := GetConfig()
a.Same(config1, config2, "配置应该是单例")
}
func TestSameCache(t *testing.T) {
a := assert.New(t)
cache := NewCache()
// 第一次获取,创建对象
user1 := cache.Get("user1")
a.NotNil(user1)
// 第二次获取,应该返回缓存的对象
user2 := cache.Get("user1")
a.Same(user1, user2, "应该返回缓存的对象")
// 不同的key,返回不同的对象
user3 := cache.Get("user2")
a.NotSame(user1, user3)
}
func TestSameSlices(t *testing.T) {
a := assert.New(t)
slice1 := []int{1, 2, 3}
slice2 := slice1 // 共享底层数组
slice3 := []int{1, 2, 3} // 新切片
// slice1和slice2共享底层数组,但切片头不同
// Same检查切片头的地址
a.Equal(slice1, slice2)
// a.Same(&slice1, &slice2) // 需要比较切片头的地址
// slice1和slice3值相等但是不同的切片
a.Equal(slice1, slice3)
a.NotSame(&slice1, &slice3)
}
type User struct {
Name string
}
var configInstance *Config
type Config struct {
Host string
}
func GetConfig() *Config {
if configInstance == nil {
configInstance = &Config{Host: "localhost"}
}
return configInstance
}
type Cache struct {
data map[string]*User
}
func NewCache() *Cache {
return &Cache{data: make(map[string]*User)}
}
func (c *Cache) Get(key string) *User {
if user, exists := c.data[key]; exists {
return user
}
user := &User{Name: key}
c.data[key] = user
return user
}
---
3.3 集合断言
01.Len断言
a.功能说明
Len断言检查集合的长度,支持字符串、数组、切片、map、channel等所有具有长度概念的类型。Len是最常用的集合断言之一,用于验证集合大小符合预期。相比手动检查len()然后用Equal断言,Len提供更清晰的错误信息。
b.使用场景
Len适用于验证查询结果数量,检查批处理的元素个数,确认过滤后的集合大小,验证分页数据的条数。在测试数据操作时,Len是必不可少的断言方法。
c.代码示例
---
// Len断言详解
package length
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLenBasics(t *testing.T) {
a := assert.New(t)
// 字符串长度
a.Len("hello", 5)
a.Len("", 0)
a.Len("你好世界", 4) // UTF-8字符数
// 切片长度
a.Len([]int{1, 2, 3}, 3)
a.Len([]int{}, 0)
a.Len([]string{"a", "b", "c", "d"}, 4)
// 数组长度
arr := [5]int{1, 2, 3, 4, 5}
a.Len(arr, 5)
// map长度
m := map[string]int{"a": 1, "b": 2}
a.Len(m, 2)
// channel长度(缓冲区中的元素数)
ch := make(chan int, 10)
a.Len(ch, 0)
ch <- 1
ch <- 2
a.Len(ch, 2)
}
func TestLenScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:查询结果验证
users := QueryUsers()
a.Len(users, 3, "应该返回3个用户")
// 场景2:过滤操作验证
numbers := []int{1, 2, 3, 4, 5, 6}
evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
a.Len(evens, 3, "应该有3个偶数")
// 场景3:分页数据验证
pageSize := 10
items := GetPage(1, pageSize)
a.Len(items, pageSize, "每页应该有10条数据")
// 场景4:批处理验证
batch := []string{"item1", "item2", "item3"}
results := ProcessBatch(batch)
a.Len(results, len(batch), "结果数量应该等于输入数量")
}
func TestLenEmpty(t *testing.T) {
a := assert.New(t)
// 空集合
a.Len([]int{}, 0)
a.Len("", 0)
a.Len(map[string]int{}, 0)
// 也可以使用Empty断言
a.Empty([]int{})
a.Empty("")
a.Empty(map[string]int{})
}
func TestLenAfterOperations(t *testing.T) {
a := assert.New(t)
// 添加操作
list := []int{1, 2, 3}
a.Len(list, 3)
list = append(list, 4, 5)
a.Len(list, 5)
// 删除操作
list = list[:3]
a.Len(list, 3)
// map操作
m := make(map[string]int)
a.Len(m, 0)
m["a"] = 1
m["b"] = 2
a.Len(m, 2)
delete(m, "a")
a.Len(m, 1)
}
// 辅助函数
type User struct{ Name string }
func QueryUsers() []User {
return []User{{Name: "Alice"}, {Name: "Bob"}, {Name: "Charlie"}}
}
func Filter(nums []int, fn func(int) bool) []int {
result := []int{}
for _, n := range nums {
if fn(n) {
result = append(result, n)
}
}
return result
}
func GetPage(page, size int) []string {
items := make([]string, size)
for i := 0; i < size; i++ {
items[i] = fmt.Sprintf("item%d", i)
}
return items
}
func ProcessBatch(items []string) []string {
return items
}
---
02.Empty和NotEmpty断言
a.功能说明
Empty断言检查集合是否为空,NotEmpty检查不为空。Empty不仅检查长度为0,还检查零值。对于字符串、切片、数组、map,Empty检查长度为0。对于指针、接口,Empty检查是否为nil。对于数值,Empty检查是否为0。
b.Empty vs Len
Empty和Len(x, 0)效果相似但语义不同。Empty表达"这个集合是空的",Len(x, 0)表达"这个集合的长度是0"。Empty还能处理nil切片和空切片,而Len只能处理非nil值。使用Empty让测试意图更清晰。
c.代码示例
---
// Empty和NotEmpty断言详解
package empty
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEmpty(t *testing.T) {
a := assert.New(t)
// 空字符串
a.Empty("")
a.NotEmpty("hello")
// 空切片
a.Empty([]int{})
var nilSlice []int
a.Empty(nilSlice) // nil切片也是空的
a.NotEmpty([]int{1, 2, 3})
// 空map
a.Empty(map[string]int{})
var nilMap map[string]int
a.Empty(nilMap) // nil map也是空的
a.NotEmpty(map[string]int{"a": 1})
// 零值
a.Empty(0)
a.Empty(false)
a.Empty(0.0)
a.NotEmpty(1)
a.NotEmpty(true)
}
func TestEmptyScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:搜索结果为空
results := Search("nonexistent")
a.Empty(results, "应该没有搜索结果")
// 场景2:过滤后为空
numbers := []int{1, 3, 5, 7}
evens := FilterEvens(numbers)
a.Empty(evens, "没有偶数")
// 场景3:清空操作
list := []string{"a", "b", "c"}
a.NotEmpty(list)
list = []string{}
a.Empty(list, "列表应该被清空")
// 场景4:可选字段为空
config := Config{
Host: "localhost",
// Port为零值
}
a.NotEmpty(config.Host)
a.Empty(config.Port, "Port未设置")
}
func TestEmptyVsNil(t *testing.T) {
a := assert.New(t)
// nil切片
var nilSlice []int
a.Nil(nilSlice)
a.Empty(nilSlice)
// 空切片(不是nil)
emptySlice := []int{}
a.NotNil(emptySlice)
a.Empty(emptySlice)
// Empty可以处理两种情况
// 但Nil只能检查nil
}
type Config struct {
Host string
Port int
}
func Search(keyword string) []string {
return []string{}
}
func FilterEvens(nums []int) []int {
result := []int{}
for _, n := range nums {
if n%2 == 0 {
result = append(result, n)
}
}
return result
}
---
03.Contains断言
a.功能说明
Contains断言检查集合是否包含指定元素。对于字符串,检查是否包含子串。对于切片和数组,检查是否包含指定元素。对于map,检查是否包含指定的键。Contains是实现集合成员检查的便捷方法。
b.NotContains
NotContains检查集合不包含指定元素,是Contains的反向断言。适用于验证黑名单、排除特定项、确认删除操作等场景。
c.代码示例
---
// Contains断言详解
package contains
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestContainsString(t *testing.T) {
a := assert.New(t)
text := "Hello, World!"
a.Contains(text, "World")
a.Contains(text, "Hello")
a.Contains(text, ", ")
a.NotContains(text, "Goodbye")
// 大小写敏感
a.Contains("Hello", "Hell")
a.NotContains("Hello", "hell")
}
func TestContainsSlice(t *testing.T) {
a := assert.New(t)
numbers := []int{1, 2, 3, 4, 5}
a.Contains(numbers, 3)
a.Contains(numbers, 1)
a.Contains(numbers, 5)
a.NotContains(numbers, 10)
// 字符串切片
names := []string{"Alice", "Bob", "Charlie"}
a.Contains(names, "Bob")
a.NotContains(names, "David")
}
func TestContainsMap(t *testing.T) {
a := assert.New(t)
// map包含检查键
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
a.Contains(m, "a")
a.Contains(m, "b")
a.NotContains(m, "d")
}
func TestContainsScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:权限检查
userPermissions := []string{"read", "write", "delete"}
a.Contains(userPermissions, "write", "用户应该有写权限")
a.NotContains(userPermissions, "admin", "用户不应该有管理员权限")
// 场景2:标签检查
tags := []string{"go", "testing", "unit-test"}
a.Contains(tags, "testing")
// 场景3:错误消息检查
errMsg := "failed to connect to database"
a.Contains(errMsg, "database")
a.Contains(errMsg, "failed")
// 场景4:配置键检查
config := map[string]string{
"host": "localhost",
"port": "8080",
}
a.Contains(config, "host")
a.NotContains(config, "password")
}
func TestContainsStructSlice(t *testing.T) {
a := assert.New(t)
type User struct {
ID int
Name string
}
users := []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Charlie"},
}
// 直接比较结构体
a.Contains(users, User{ID: 2, Name: "Bob"})
a.NotContains(users, User{ID: 4, Name: "David"})
}
---
04.ElementsMatch断言
a.功能说明
ElementsMatch断言检查两个切片包含相同的元素,但忽略顺序。这对于测试返回无序结果的函数非常有用,如查询数据库、并发处理、集合操作等。ElementsMatch还能正确处理重复元素。
b.ElementsMatch vs Equal
Equal要求元素顺序完全一致,ElementsMatch忽略顺序只检查元素集合。当函数返回的顺序不确定时,使用ElementsMatch避免测试不稳定。如果顺序很重要,使用Equal。
c.代码示例
---
// ElementsMatch断言详解
package elementsmatch
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestElementsMatchBasics(t *testing.T) {
a := assert.New(t)
// 相同元素,不同顺序
a.ElementsMatch(
[]int{1, 2, 3},
[]int{3, 2, 1},
)
// 相同元素,顺序相同
a.ElementsMatch(
[]int{1, 2, 3},
[]int{1, 2, 3},
)
// 包含重复元素
a.ElementsMatch(
[]int{1, 2, 2, 3},
[]int{2, 3, 1, 2},
)
// 不同元素
// a.ElementsMatch([]int{1, 2, 3}, []int{1, 2, 4}) // 失败
}
func TestElementsMatchStrings(t *testing.T) {
a := assert.New(t)
expected := []string{"apple", "banana", "cherry"}
actual := []string{"cherry", "apple", "banana"}
a.ElementsMatch(expected, actual)
}
func TestElementsMatchScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:数据库查询(结果顺序不确定)
expected := []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Charlie"},
}
actual := QueryUsers() // 返回顺序可能不同
a.ElementsMatch(expected, actual)
// 场景2:并发处理结果
expected2 := []int{1, 2, 3, 4, 5}
actual2 := ProcessConcurrently([]int{1, 2, 3, 4, 5})
a.ElementsMatch(expected2, actual2)
// 场景3:集合操作
set1 := []string{"a", "b", "c"}
set2 := []string{"c", "a", "b"}
a.ElementsMatch(set1, set2)
}
func TestElementsMatchWithDuplicates(t *testing.T) {
a := assert.New(t)
// 重复元素的数量必须相同
a.ElementsMatch(
[]int{1, 1, 2, 2, 3},
[]int{2, 1, 3, 2, 1},
)
// 重复次数不同会失败
// a.ElementsMatch(
// []int{1, 1, 2},
// []int{1, 2, 2},
// ) // 失败
}
type User struct {
ID int
Name string
}
func QueryUsers() []User {
// 模拟数据库查询,顺序不确定
return []User{
{ID: 2, Name: "Bob"},
{ID: 1, Name: "Alice"},
{ID: 3, Name: "Charlie"},
}
}
func ProcessConcurrently(items []int) []int {
// 模拟并发处理,返回顺序不确定
return []int{5, 3, 1, 4, 2}
}
---
05.Subset和NotSubset断言
a.功能说明
Subset断言检查一个集合是否是另一个集合的子集,即第一个集合的所有元素都在第二个集合中。NotSubset检查不是子集关系。子集断言适合验证过滤操作、权限检查、配置验证等场景。
b.子集关系
空集合是任何集合的子集。集合是自身的子集。子集关系不关心顺序,只关心元素是否都存在。子集可以有重复元素,但重复次数不能超过父集合。
c.代码示例
---
// Subset断言详解
package subset
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSubsetBasics(t *testing.T) {
a := assert.New(t)
all := []int{1, 2, 3, 4, 5}
// 子集
a.Subset(all, []int{2, 3})
a.Subset(all, []int{1, 5})
a.Subset(all, []int{1, 2, 3, 4, 5}) // 自身是子集
a.Subset(all, []int{}) // 空集是子集
// 不是子集
a.NotSubset(all, []int{1, 6}) // 6不在all中
a.NotSubset(all, []int{10})
}
func TestSubsetScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:权限验证
allPermissions := []string{"read", "write", "delete", "admin"}
userPermissions := []string{"read", "write"}
a.Subset(allPermissions, userPermissions, "用户权限应该是有效权限的子集")
// 场景2:过滤操作验证
allItems := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
filtered := FilterGreaterThan(allItems, 5)
a.Subset(allItems, filtered, "过滤结果应该是原集合的子集")
// 场景3:配置项验证
validOptions := []string{"debug", "info", "warn", "error"}
userOptions := []string{"info", "error"}
a.Subset(validOptions, userOptions, "用户选项应该是有效选项的子集")
}
func TestSubsetStrings(t *testing.T) {
a := assert.New(t)
allTags := []string{"go", "python", "java", "javascript", "rust"}
projectTags := []string{"go", "rust"}
a.Subset(allTags, projectTags)
}
func FilterGreaterThan(nums []int, threshold int) []int {
result := []int{}
for _, n := range nums {
if n > threshold {
result = append(result, n)
}
}
return result
}
---
3.4 类型断言
01.IsType断言
a.功能说明
IsType断言检查值的类型是否与指定类型相同。使用reflect包进行类型检查,支持所有Go类型包括基本类型、结构体、接口、指针等。IsType在测试接口实现、类型转换、反序列化等场景中非常有用。
b.类型匹配规则
IsType进行精确的类型匹配,不进行类型转换。int32和int64被认为是不同类型。*User和User被认为是不同类型。interface{}可以匹配任何类型,但IsType检查的是实际存储的类型。
c.代码示例
---
// IsType断言详解
package istype
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsTypeBasics(t *testing.T) {
a := assert.New(t)
// 基本类型
a.IsType(0, 42) // int
a.IsType("", "hello") // string
a.IsType(false, true) // bool
a.IsType(0.0, 3.14) // float64
a.IsType(int32(0), int32(42)) // int32
a.IsType(int64(0), int64(100)) // int64
// 不同的整数类型不匹配
// a.IsType(int32(0), 42) // 失败:int32 != int
// a.IsType(int64(0), 42) // 失败:int64 != int
}
func TestIsTypeStructs(t *testing.T) {
a := assert.New(t)
type User struct {
Name string
Age int
}
user := User{Name: "Alice", Age: 25}
// 结构体类型
a.IsType(User{}, user)
// 结构体指针类型
a.IsType(&User{}, &user)
// 结构体和指针是不同类型
// a.IsType(User{}, &user) // 失败
}
func TestIsTypeSlicesAndMaps(t *testing.T) {
a := assert.New(t)
// 切片类型
a.IsType([]int{}, []int{1, 2, 3})
a.IsType([]string{}, []string{"a", "b"})
// map类型
a.IsType(map[string]int{}, map[string]int{"a": 1})
// 不同的切片类型不匹配
// a.IsType([]int{}, []string{}) // 失败
}
func TestIsTypeInterfaces(t *testing.T) {
a := assert.New(t)
type Reader interface {
Read(p []byte) (n int, err error)
}
var r Reader
var w io.Writer
// interface{}类型
var any interface{} = 42
a.IsType(0, any) // 检查实际存储的int类型
any = "hello"
a.IsType("", any) // 检查实际存储的string类型
}
func TestIsTypeScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:JSON反序列化验证
var result interface{}
json.Unmarshal([]byte(`{"name":"Alice"}`), &result)
a.IsType(map[string]interface{}{}, result)
// 场景2:工厂函数返回类型
obj := CreateObject("user")
a.IsType(&User{}, obj)
// 场景3:类型转换验证
var data interface{} = []int{1, 2, 3}
a.IsType([]int{}, data)
// 可以安全地进行类型断言
if slice, ok := data.([]int); ok {
a.Len(slice, 3)
}
}
type User struct {
Name string
Age int
}
func CreateObject(objType string) interface{} {
switch objType {
case "user":
return &User{}
default:
return nil
}
}
---
02.Implements断言
a.功能说明
Implements断言检查类型是否实现了指定的接口。这对于验证接口实现、测试多态性、确保API契约等场景非常重要。Implements可以在编译期无法检查接口实现的动态场景中使用。
b.接口检查语法
Implements的第一个参数必须是接口类型的nil指针,使用(*InterfaceName)(nil)语法。第二个参数是要检查的值或指针。如果类型实现了接口的所有方法,断言通过。
c.代码示例
---
// Implements断言详解
package implements
import (
"testing"
"github.com/stretchr/testify/assert"
"io"
)
func TestImplementsBasics(t *testing.T) {
a := assert.New(t)
// bytes.Buffer实现了io.Writer接口
var buf bytes.Buffer
a.Implements((*io.Writer)(nil), &buf)
// bytes.Buffer实现了io.Reader接口
a.Implements((*io.Reader)(nil), &buf)
// strings.Reader实现了io.Reader接口
reader := strings.NewReader("hello")
a.Implements((*io.Reader)(nil), reader)
}
func TestImplementsCustomInterface(t *testing.T) {
a := assert.New(t)
// 定义接口
type Validator interface {
Validate() error
}
type Saver interface {
Save() error
}
// 定义实现
type User struct {
Name string
}
func (u *User) Validate() error {
if u.Name == "" {
return errors.New("name is required")
}
return nil
}
func (u *User) Save() error {
// 保存逻辑
return nil
}
user := &User{Name: "Alice"}
// 验证接口实现
a.Implements((*Validator)(nil), user)
a.Implements((*Saver)(nil), user)
}
func TestImplementsScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:插件系统验证
plugin := LoadPlugin("user-plugin")
a.Implements((*Plugin)(nil), plugin, "插件必须实现Plugin接口")
// 场景2:驱动程序验证
driver := GetDriver("mysql")
a.Implements((*Driver)(nil), driver, "驱动必须实现Driver接口")
// 场景3:中间件验证
middleware := CreateMiddleware()
a.Implements((*Middleware)(nil), middleware)
}
func TestNotImplements(t *testing.T) {
a := assert.New(t)
type Writer interface {
Write(p []byte) (n int, err error)
}
type SimpleStruct struct {
Value int
}
obj := &SimpleStruct{Value: 42}
// SimpleStruct不实现Writer接口
a.NotImplements((*Writer)(nil), obj)
}
// 辅助接口和类型
type Plugin interface {
Init() error
Execute() error
}
type Driver interface {
Connect() error
Query(sql string) (interface{}, error)
}
type Middleware interface {
Handle(next func()) error
}
type UserPlugin struct{}
func (p *UserPlugin) Init() error { return nil }
func (p *UserPlugin) Execute() error { return nil }
func LoadPlugin(name string) Plugin {
return &UserPlugin{}
}
type MySQLDriver struct{}
func (d *MySQLDriver) Connect() error { return nil }
func (d *MySQLDriver) Query(sql string) (interface{}, error) { return nil, nil }
func GetDriver(name string) Driver {
return &MySQLDriver{}
}
type SimpleMiddleware struct{}
func (m *SimpleMiddleware) Handle(next func()) error { return nil }
func CreateMiddleware() Middleware {
return &SimpleMiddleware{}
}
---
03.Kind检查
a.reflect.Kind
虽然testify没有直接的Kind断言,但可以结合reflect.Kind进行类型分类检查。Kind表示类型的基础分类如Int、String、Struct、Ptr等。Kind检查适合需要判断类型大类而非精确类型的场景。
b.Kind使用
---
// Kind检查示例
package kind
import (
"testing"
"github.com/stretchr/testify/assert"
"reflect"
)
func TestKindChecks(t *testing.T) {
a := assert.New(t)
// 使用reflect.Kind检查类型分类
a.Equal(reflect.Int, reflect.TypeOf(42).Kind())
a.Equal(reflect.String, reflect.TypeOf("hello").Kind())
a.Equal(reflect.Bool, reflect.TypeOf(true).Kind())
a.Equal(reflect.Float64, reflect.TypeOf(3.14).Kind())
// 结构体
type User struct{ Name string }
a.Equal(reflect.Struct, reflect.TypeOf(User{}).Kind())
// 指针
user := &User{}
a.Equal(reflect.Ptr, reflect.TypeOf(user).Kind())
// 切片
a.Equal(reflect.Slice, reflect.TypeOf([]int{}).Kind())
// map
a.Equal(reflect.Map, reflect.TypeOf(map[string]int{}).Kind())
}
func TestKindScenarios(t *testing.T) {
a := assert.New(t)
// 场景:动态类型检查
data := GetDynamicData()
kind := reflect.TypeOf(data).Kind()
switch kind {
case reflect.Slice:
a.True(true, "数据是切片类型")
case reflect.Map:
a.True(false, "数据不应该是map类型")
default:
a.Fail("未知的数据类型")
}
}
func GetDynamicData() interface{} {
return []int{1, 2, 3}
}
---
3.5 错误断言
01.Error和NoError断言
a.功能说明
Error断言检查错误不为nil,NoError检查错误为nil。这是最基础也是最常用的错误检查方法。在Go语言中,错误处理是显式的,几乎每个可能失败的操作都会返回error,因此错误断言在测试中无处不在。
b.使用场景
NoError用于验证操作成功完成,没有产生错误。Error用于验证错误场景,确保函数正确地返回了错误。两者配合使用可以完整测试函数的正常路径和异常路径。
c.代码示例
---
// Error和NoError断言详解
package error
import (
"testing"
"github.com/stretchr/testify/assert"
"errors"
"os"
)
func TestNoError(t *testing.T) {
a := assert.New(t)
// 成功操作,无错误
err := DoSomethingSuccess()
a.NoError(err)
// 文件操作成功
file, err := os.Open("testfile.txt")
if err == nil {
defer file.Close()
a.NoError(err)
}
// 数据验证成功
user := &User{Name: "Alice", Age: 25}
err = ValidateUser(user)
a.NoError(err, "有效用户不应该产生错误")
}
func TestError(t *testing.T) {
a := assert.New(t)
// 失败操作,有错误
err := DoSomethingFail()
a.Error(err, "应该返回错误")
// 文件不存在
_, err = os.Open("nonexistent.txt")
a.Error(err)
// 数据验证失败
invalidUser := &User{Name: "", Age: -1}
err = ValidateUser(invalidUser)
a.Error(err, "无效用户应该产生错误")
}
func TestErrorScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:数据库操作
db := ConnectDB()
if db != nil {
defer db.Close()
err := db.Insert(&User{Name: "Alice"})
a.NoError(err, "插入应该成功")
_, err = db.Query("invalid sql")
a.Error(err, "无效SQL应该返回错误")
}
// 场景2:API调用
response, err := CallAPI("valid-endpoint")
a.NoError(err)
a.NotNil(response)
_, err = CallAPI("invalid-endpoint")
a.Error(err)
// 场景3:文件处理
err = ProcessFile("valid.txt")
a.NoError(err)
err = ProcessFile("invalid.txt")
a.Error(err)
}
type User struct {
Name string
Age int
}
func DoSomethingSuccess() error {
return nil
}
func DoSomethingFail() error {
return errors.New("operation failed")
}
func ValidateUser(user *User) error {
if user.Name == "" {
return errors.New("name is required")
}
if user.Age < 0 {
return errors.New("age must be positive")
}
return nil
}
type DB struct{}
func ConnectDB() *DB { return &DB{} }
func (db *DB) Close() {}
func (db *DB) Insert(u *User) error { return nil }
func (db *DB) Query(sql string) (interface{}, error) {
if sql == "invalid sql" {
return nil, errors.New("syntax error")
}
return nil, nil
}
func CallAPI(endpoint string) (interface{}, error) {
if endpoint == "invalid-endpoint" {
return nil, errors.New("404 not found")
}
return map[string]string{"status": "ok"}, nil
}
func ProcessFile(filename string) error {
if filename == "invalid.txt" {
return errors.New("file not found")
}
return nil
}
---
02.EqualError断言
a.功能说明
EqualError断言检查错误消息是否与指定字符串完全相等。这用于精确验证错误消息内容,确保错误信息正确且一致。EqualError比Error更严格,不仅要求有错误,还要求错误消息精确匹配。
b.使用场景
EqualError适合测试自定义错误类型,验证错误消息格式,确保API错误响应一致。在测试用户可见的错误消息时特别有用,可以确保错误提示清晰准确。
c.代码示例
---
// EqualError断言详解
package equalerror
import (
"testing"
"github.com/stretchr/testify/assert"
"errors"
"fmt"
)
func TestEqualError(t *testing.T) {
a := assert.New(t)
// 精确匹配错误消息
err := errors.New("file not found")
a.EqualError(err, "file not found")
// 格式化错误消息
filename := "test.txt"
err2 := fmt.Errorf("failed to open %s", filename)
a.EqualError(err2, "failed to open test.txt")
}
func TestEqualErrorValidation(t *testing.T) {
a := assert.New(t)
// 验证错误消息
err := ValidateEmail("invalid")
a.EqualError(err, "invalid email format")
err = ValidateAge(-1)
a.EqualError(err, "age must be between 0 and 150")
err = ValidatePassword("123")
a.EqualError(err, "password must be at least 8 characters")
}
func TestEqualErrorCustomTypes(t *testing.T) {
a := assert.New(t)
// 自定义错误类型
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
err := &ValidationError{
Field: "email",
Message: "invalid format",
}
a.EqualError(err, "email: invalid format")
}
func ValidateEmail(email string) error {
if !strings.Contains(email, "@") {
return errors.New("invalid email format")
}
return nil
}
func ValidateAge(age int) error {
if age < 0 || age > 150 {
return errors.New("age must be between 0 and 150")
}
return nil
}
func ValidatePassword(password string) error {
if len(password) < 8 {
return errors.New("password must be at least 8 characters")
}
return nil
}
---
03.ErrorIs和ErrorAs断言
a.Go 1.13错误链
Go 1.13引入了错误包装机制,使用fmt.Errorf的%w动词可以包装错误,形成错误链。ErrorIs检查错误链中是否包含特定错误,ErrorAs检查错误链中是否有可以转换为特定类型的错误。
b.错误链场景
错误包装用于添加上下文信息而不丢失原始错误。ErrorIs适合检查是否是特定的标准错误如io.EOF、os.ErrNotExist。ErrorAs适合提取自定义错误类型并访问其字段。
c.代码示例
---
// ErrorIs和ErrorAs断言详解
package errorchain
import (
"testing"
"github.com/stretchr/testify/assert"
"errors"
"fmt"
"os"
)
func TestErrorIs(t *testing.T) {
a := assert.New(t)
// 定义标准错误
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")
// 包装错误
err := fmt.Errorf("failed to get user: %w", ErrNotFound)
// ErrorIs检查错误链
a.ErrorIs(err, ErrNotFound)
a.NotErrorIs(err, ErrPermission)
// 多层包装
wrappedTwice := fmt.Errorf("operation failed: %w", err)
a.ErrorIs(wrappedTwice, ErrNotFound) // 仍然能检测到
}
func TestErrorIsStandard(t *testing.T) {
a := assert.New(t)
// 标准库错误
_, err := os.Open("nonexistent.txt")
a.ErrorIs(err, os.ErrNotExist)
// 包装标准错误
wrappedErr := fmt.Errorf("failed to read config: %w", err)
a.ErrorIs(wrappedErr, os.ErrNotExist)
}
func TestErrorAs(t *testing.T) {
a := assert.New(t)
// 自定义错误类型
type ValidationError struct {
Field string
Message string
Code int
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("[%d] %s: %s", e.Code, e.Field, e.Message)
}
// 创建并包装自定义错误
valErr := &ValidationError{
Field: "email",
Message: "invalid format",
Code: 400,
}
wrappedErr := fmt.Errorf("validation failed: %w", valErr)
// ErrorAs提取自定义错误
var target *ValidationError
a.ErrorAs(wrappedErr, &target)
// 访问自定义错误的字段
a.Equal("email", target.Field)
a.Equal("invalid format", target.Message)
a.Equal(400, target.Code)
}
func TestErrorChainScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:数据库错误链
var ErrDBConnection = errors.New("database connection failed")
err := ConnectDatabase()
a.ErrorIs(err, ErrDBConnection)
// 场景2:多层业务错误
err2 := ProcessOrder(123)
if err2 != nil {
var paymentErr *PaymentError
if a.ErrorAs(err2, &paymentErr) {
a.Equal("insufficient funds", paymentErr.Reason)
}
}
}
var ErrDBConnection = errors.New("database connection failed")
func ConnectDatabase() error {
return fmt.Errorf("failed to initialize: %w", ErrDBConnection)
}
type PaymentError struct {
Reason string
Amount float64
}
func (e *PaymentError) Error() string {
return fmt.Sprintf("payment failed: %s (amount: %.2f)", e.Reason, e.Amount)
}
func ProcessOrder(orderID int) error {
payErr := &PaymentError{
Reason: "insufficient funds",
Amount: 99.99,
}
return fmt.Errorf("order %d: %w", orderID, payErr)
}
---
04.ErrorContains断言
a.功能说明
ErrorContains断言检查错误消息是否包含指定的子字符串。相比EqualError的精确匹配,ErrorContains更灵活,只需要错误消息包含关键词即可。适合测试错误消息的关键信息而不关心完整格式。
b.使用场景
ErrorContains适合测试动态错误消息,验证错误消息包含关键信息,测试第三方库返回的错误。当错误消息格式可能变化但关键词保持不变时,使用ErrorContains比EqualError更稳定。
c.代码示例
---
// ErrorContains断言详解
package errorcontains
import (
"testing"
"github.com/stretchr/testify/assert"
"fmt"
"errors"
)
func TestErrorContains(t *testing.T) {
a := assert.New(t)
// 检查错误消息包含关键词
err := errors.New("failed to connect to database: connection timeout")
a.ErrorContains(err, "database")
a.ErrorContains(err, "timeout")
a.ErrorContains(err, "connect")
// 动态错误消息
filename := "config.json"
err2 := fmt.Errorf("failed to read file %s: permission denied", filename)
a.ErrorContains(err2, filename)
a.ErrorContains(err2, "permission")
}
func TestErrorContainsScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:API错误
err := CallExternalAPI()
if err != nil {
a.ErrorContains(err, "API")
a.ErrorContains(err, "timeout")
}
// 场景2:验证错误
err = ValidateForm(map[string]string{})
a.ErrorContains(err, "required")
// 场景3:数据库错误
err = QueryDatabase("invalid sql")
a.ErrorContains(err, "syntax")
}
func CallExternalAPI() error {
return fmt.Errorf("API call failed: request timeout after 30s")
}
func ValidateForm(form map[string]string) error {
if form["name"] == "" {
return errors.New("name field is required")
}
return nil
}
func QueryDatabase(sql string) error {
return errors.New("SQL syntax error: unexpected token")
}
---
01.Panics和NotPanics断言 a.功能说明 Panics断言检查函数执行时是否发生panic,NotPanics检查函数不会panic。在Go语言中,panic用于表示不可恢复的错误,测试panic行为确保程序在异常情况下的正确性。testify提供安全的方式来测试panic,避免测试进程崩溃。 b.Panic测试原理 Panics和NotPanics通过defer recover机制捕获panic。测试代码将待测函数包装在匿名函数中执行,通过recover捕获panic并验证。这使得panic测试安全可控,不会导致测试中断。 c.代码示例 ``` // Panics和NotPanics断言详解 package panic
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPanicsBasics(t *testing.T) {
a := assert.New(t)
// 测试会panic的函数
a.Panics(func() {
panic("something went wrong")
})
// 测试不会panic的函数
a.NotPanics(func() {
_ = 1 + 1
})
// 除零会panic
a.Panics(func() {
x := 0
_ = 10 / x
})
// 访问nil指针会panic
a.Panics(func() {
var ptr *int
_ = *ptr
})
// 访问越界会panic
a.Panics(func() {
arr := []int{1, 2, 3}
_ = arr[10]
})
}
func TestPanicsScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:参数验证导致panic
a.Panics(func() {
MustValidate(nil)
}, "nil参数应该panic")
// 场景2:初始化失败导致panic
a.Panics(func() {
InitializeWithInvalidConfig()
})
// 场景3:资源不可用导致panic
a.Panics(func() {
MustConnect("invalid-host")
})
}
func TestNotPanicsScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:正常参数不应panic
a.NotPanics(func() {
MustValidate(&User{Name: "Alice"})
})
// 场景2:有效配置不应panic
a.NotPanics(func() {
InitializeWithValidConfig()
})
// 场景3:正常操作不应panic
a.NotPanics(func() {
result := SafeOperation()
_ = result
})
}
func TestPanicRecovery(t *testing.T) {
a := assert.New(t)
// 测试panic恢复机制
a.Panics(func() {
defer func() {
if r := recover(); r != nil {
// 恢复但重新panic以便测试捕获
panic(r)
}
}()
panic("test panic")
})
}
type User struct {
Name string
}
func MustValidate(user *User) {
if user == nil {
panic("user cannot be nil")
}
if user.Name == "" {
panic("name is required")
}
}
func InitializeWithInvalidConfig() {
panic("invalid configuration")
}
func InitializeWithValidConfig() {
// 正常初始化
}
func MustConnect(host string) {
if host == "invalid-host" {
panic("failed to connect")
}
}
func SafeOperation() int {
return 42
}
```
02.PanicValue断言 a.功能说明 PanicsWithValue断言不仅检查函数是否panic,还检查panic的值是否与指定值相等。PanicsWithError检查panic的值是否是error类型且消息匹配。这些方法用于精确验证panic的内容。 b.Panic值类型 Go语言的panic可以传递任何类型的值,常见的有string、error、自定义类型等。PanicsWithValue进行精确匹配,PanicsWithError专门处理error类型的panic。 c.代码示例 ``` // Panic值断言详解 package panicvalue
import (
"testing"
"github.com/stretchr/testify/assert"
"errors"
)
func TestPanicsWithValue(t *testing.T) {
a := assert.New(t)
// 字符串panic值
a.PanicsWithValue("expected panic", func() {
panic("expected panic")
})
// 整数panic值
a.PanicsWithValue(42, func() {
panic(42)
})
// 结构体panic值
type PanicData struct {
Code int
Message string
}
expected := PanicData{Code: 500, Message: "server error"}
a.PanicsWithValue(expected, func() {
panic(expected)
})
}
func TestPanicsWithError(t *testing.T) {
a := assert.New(t)
// error类型的panic
expectedErr := errors.New("critical error")
a.PanicsWithError(expectedErr, func() {
panic(expectedErr)
})
// 自定义error类型
type CustomError struct {
Code int
Msg string
}
func (e *CustomError) Error() string {
return e.Msg
}
customErr := &CustomError{Code: 404, Msg: "not found"}
a.PanicsWithError(customErr, func() {
panic(customErr)
})
}
func TestPanicValueScenarios(t *testing.T) {
a := assert.New(t)
// 场景1:断言失败panic
a.PanicsWithValue("assertion failed: x must be positive", func() {
Assert(false, "x must be positive")
})
// 场景2:配置错误panic
a.PanicsWithError(errors.New("invalid config"), func() {
LoadConfig("invalid.yaml")
})
// 场景3:资源耗尽panic
type ResourceError struct {
Resource string
Limit int
}
expected := ResourceError{Resource: "memory", Limit: 1024}
a.PanicsWithValue(expected, func() {
AllocateResources(2048)
})
}
func Assert(condition bool, message string) {
if !condition {
panic("assertion failed: " + message)
}
}
func LoadConfig(filename string) {
if filename == "invalid.yaml" {
panic(errors.New("invalid config"))
}
}
type ResourceError struct {
Resource string
Limit int
}
func AllocateResources(size int) {
if size > 1024 {
panic(ResourceError{Resource: "memory", Limit: 1024})
}
}
```
03.安全panic测试 a.测试隔离 panic测试需要确保测试隔离,一个测试的panic不应该影响其他测试。testify的Panics断言通过recover机制实现隔离,每个panic测试在独立的goroutine中执行。 b.最佳实践 测试panic时应该明确panic的触发条件,验证panic消息或值的准确性,确保只在必要时使用panic。对于可恢复的错误应该使用error返回,只有真正不可恢复的情况才使用panic。 c.代码示例 ``` // 安全panic测试最佳实践 package safepanic
import (
"testing"
"github.com/stretchr/testify/assert"
"errors"
)
func TestSafePanicTesting(t *testing.T) {
a := assert.New(t)
// 测试1:验证panic条件
a.Panics(func() {
ValidatePositive(-1)
}, "负数应该panic")
a.NotPanics(func() {
ValidatePositive(1)
}, "正数不应该panic")
// 测试2:验证panic消息
a.PanicsWithValue("value must be positive", func() {
ValidatePositive(-1)
})
// 测试3:测试后程序继续运行
a.Equal(42, 42, "测试在panic测试后继续")
}
func TestMultiplePanicTests(t *testing.T) {
a := assert.New(t)
// 多个panic测试互不影响
a.Panics(func() { panic("test 1") })
a.Panics(func() { panic("test 2") })
a.Panics(func() { panic("test 3") })
// 后续测试正常执行
a.Equal(1+1, 2)
}
func TestPanicVsError(t *testing.T) {
a := assert.New(t)
// 推荐:可恢复错误使用error
err := DoSomethingRecoverable()
a.Error(err)
// 只有不可恢复的情况才panic
a.Panics(func() {
DoSomethingUnrecoverable()
})
}
func ValidatePositive(value int) {
if value <= 0 {
panic("value must be positive")
}
}
func DoSomethingRecoverable() error {
// 可恢复的错误返回error
return errors.New("recoverable error")
}
func DoSomethingUnrecoverable() {
// 不可恢复的错误使用panic
panic("unrecoverable error")
}
```
04.Panic测试模式 a.Must函数测试 Go语言中的Must函数模式在初始化失败时panic。测试Must函数需要验证正常情况不panic,异常情况正确panic。Must函数通常用于程序启动阶段的配置验证。 b.不变量断言 使用panic实现不变量断言,在违反程序不变量时panic。测试这类断言需要构造违反不变量的场景,验证panic被正确触发。 c.代码示例 ``` // Panic测试模式 package panicpattern
import (
"testing"
"github.com/stretchr/testify/assert"
"regexp"
)
// Must函数模式
func MustCompile(pattern string) *regexp.Regexp {
r, err := regexp.Compile(pattern)
if err != nil {
panic(err)
}
return r
}
func TestMustFunctions(t *testing.T) {
a := assert.New(t)
// 有效正则表达式不应panic
a.NotPanics(func() {
r := MustCompile(`^\d+$`)
_ = r
})
// 无效正则表达式应该panic
a.Panics(func() {
_ = MustCompile(`[invalid`)
})
}
// 不变量断言模式
type Stack struct {
items []int
size int
}
func (s *Stack) Push(item int) {
s.items = append(s.items, item)
s.size++
s.assertInvariant()
}
func (s *Stack) Pop() int {
if s.size == 0 {
panic("pop from empty stack")
}
item := s.items[s.size-1]
s.items = s.items[:s.size-1]
s.size--
s.assertInvariant()
return item
}
func (s *Stack) assertInvariant() {
if len(s.items) != s.size {
panic("invariant violated: size mismatch")
}
}
func TestInvariantAssertions(t *testing.T) {
a := assert.New(t)
// 正常操作不应违反不变量
a.NotPanics(func() {
s := &Stack{}
s.Push(1)
s.Push(2)
_ = s.Pop()
})
// 从空栈pop应该panic
a.Panics(func() {
s := &Stack{}
s.Pop()
})
}
```
3.7 自定义断言
01.Condition断言
a.功能说明
Condition断言接受一个返回bool的函数,用于执行自定义的复杂条件检查。当testify提供的标准断言不能满足需求时,Condition提供了灵活的扩展方式。Condition让你可以编写任意复杂的验证逻辑,同时保持测试代码的可读性。
b.使用场景
Condition适用于需要多个条件组合判断的场景,复杂的业务规则验证,需要访问多个对象属性的检查,以及标准断言无法表达的自定义逻辑。
c.代码示例
---
// Condition断言详解
package condition
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConditionBasics(t *testing.T) {
a := assert.New(t)
// 简单条件
a.Condition(func() bool {
return 5 > 3
}, "5应该大于3")
// 复杂条件
a.Condition(func() bool {
x := 10
y := 20
return x+y == 30 && x < y
}, "复杂条件应该满足")
}
func TestConditionWithObjects(t *testing.T) {
a := assert.New(t)
user := User{
Name: "Alice",
Age: 25,
Email: "[email protected]",
}
// 验证多个字段
a.Condition(func() bool {
return len(user.Name) > 0 &&
user.Age >= 18 &&
strings.Contains(user.Email, "@")
}, "用户信息应该完整且有效")
}
func TestConditionBusinessRules(t *testing.T) {
a := assert.New(t)
order := Order{
Items: []OrderItem{{Price: 100}, {Price: 50}},
Discount: 10,
Tax: 15,
}
// 复杂业务规则验证
a.Condition(func() bool {
subtotal := 0.0
for _, item := range order.Items {
subtotal += item.Price
}
total := subtotal - order.Discount + order.Tax
return total == 155.0
}, "订单总额计算应该正确")
}
func TestConditionCollections(t *testing.T) {
a := assert.New(t)
numbers := []int{1, 2, 3, 4, 5}
// 集合条件
a.Condition(func() bool {
// 所有元素都是正数
for _, n := range numbers {
if n <= 0 {
return false
}
}
return true
}, "所有数字应该是正数")
// 集合统计条件
a.Condition(func() bool {
sum := 0
for _, n := range numbers {
sum += n
}
return sum == 15
}, "数字总和应该是15")
}
type User struct {
Name string
Age int
Email string
}
type Order struct {
Items []OrderItem
Discount float64
Tax float64
}
type OrderItem struct {
Price float64
}
---
02.自定义断言函数
a.封装重复逻辑
当某个断言逻辑需要在多个测试中重复使用时,可以将其封装为自定义断言函数。自定义断言函数接受testing.T和被测值作为参数,内部使用testify断言,提供清晰的错误信息。
b.领域特定断言
为特定领域创建专用的断言函数,如验证HTTP响应、数据库记录、配置对象等。领域特定断言让测试代码更加语义化,减少重复,提高可维护性。
c.代码示例
---
// 自定义断言函数
package customassert
import (
"testing"
"github.com/stretchr/testify/assert"
"net/http"
)
// 自定义断言:验证用户有效性
func AssertValidUser(t *testing.T, user *User) {
t.Helper()
a := assert.New(t)
a.NotNil(user, "用户不能为nil")
a.NotEmpty(user.Name, "用户名不能为空")
a.Greater(user.Age, 0, "年龄必须大于0")
a.Contains(user.Email, "@", "邮箱格式应该有效")
}
// 自定义断言:验证HTTP响应
func AssertHTTPSuccess(t *testing.T, resp *http.Response) {
t.Helper()
a := assert.New(t)
a.NotNil(resp)
a.Equal(http.StatusOK, resp.StatusCode, "应该返回200")
a.Contains(resp.Header.Get("Content-Type"), "application/json")
}
// 自定义断言:验证列表非空且有序
func AssertSortedNonEmpty(t *testing.T, numbers []int) {
t.Helper()
a := assert.New(t)
a.NotEmpty(numbers, "列表不应该为空")
for i := 1; i < len(numbers); i++ {
a.LessOrEqual(numbers[i-1], numbers[i],
"列表应该是有序的:索引%d的值(%d)应该<=索引%d的值(%d)",
i-1, numbers[i-1], i, numbers[i])
}
}
// 使用自定义断言
func TestCustomAssertions(t *testing.T) {
// 使用自定义用户断言
user := &User{
Name: "Alice",
Age: 25,
Email: "[email protected]",
}
AssertValidUser(t, user)
// 使用自定义排序断言
numbers := []int{1, 3, 5, 7, 9}
AssertSortedNonEmpty(t, numbers)
}
// 自定义断言:验证订单
func AssertValidOrder(t *testing.T, order *Order) {
t.Helper()
a := assert.New(t)
a.NotNil(order)
a.NotEmpty(order.Items, "订单必须包含商品")
a.Positive(order.TotalAmount, "订单总额必须为正")
a.NotEmpty(order.CustomerID, "必须指定客户")
// 验证每个商品
for i, item := range order.Items {
a.Positive(item.Quantity, "商品%d数量必须为正", i)
a.Positive(item.Price, "商品%d价格必须为正", i)
}
}
type User struct {
Name string
Age int
Email string
}
type Order struct {
CustomerID string
Items []OrderItem
TotalAmount float64
}
type OrderItem struct {
Quantity int
Price float64
}
---
03.断言辅助函数
a.复杂条件封装
将复杂的条件判断逻辑封装为辅助函数,返回bool和错误描述。辅助函数可以在Condition断言中使用,也可以在自定义断言函数中使用。清晰的辅助函数让测试逻辑更易理解和维护。
b.可重用验证器
创建可重用的验证器函数,接受值并返回验证结果。验证器可以组合使用,形成复杂的验证链。这种模式在测试数据验证、配置检查等场景中特别有用。
c.代码示例
---
// 断言辅助函数
package helper
import (
"testing"
"github.com/stretchr/testify/assert"
"strings"
)
// 辅助函数:验证邮箱格式
func isValidEmail(email string) (bool, string) {
if !strings.Contains(email, "@") {
return false, "邮箱必须包含@"
}
if !strings.Contains(email, ".") {
return false, "邮箱必须包含域名"
}
parts := strings.Split(email, "@")
if len(parts[0]) == 0 {
return false, "邮箱用户名不能为空"
}
return true, ""
}
// 辅助函数:验证密码强度
func isStrongPassword(password string) (bool, string) {
if len(password) < 8 {
return false, "密码长度至少8位"
}
hasUpper := false
hasLower := false
hasDigit := false
for _, c := range password {
if c >= 'A' && c <= 'Z' {
hasUpper = true
} else if c >= 'a' && c <= 'z' {
hasLower = true
} else if c >= '0' && c <= '9' {
hasDigit = true
}
}
if !hasUpper {
return false, "密码必须包含大写字母"
}
if !hasLower {
return false, "密码必须包含小写字母"
}
if !hasDigit {
return false, "密码必须包含数字"
}
return true, ""
}
// 使用辅助函数的自定义断言
func AssertValidEmail(t *testing.T, email string) {
t.Helper()
valid, reason := isValidEmail(email)
assert.True(t, valid, reason)
}
func AssertStrongPassword(t *testing.T, password string) {
t.Helper()
valid, reason := isStrongPassword(password)
assert.True(t, valid, reason)
}
// 测试使用
func TestValidationHelpers(t *testing.T) {
// 有效邮箱
AssertValidEmail(t, "[email protected]")
// 强密码
AssertStrongPassword(t, "MyP@ssw0rd")
}
// 组合验证器
type Validator func(value interface{}) (bool, string)
func CombineValidators(validators ...Validator) Validator {
return func(value interface{}) (bool, string) {
for _, validator := range validators {
if valid, reason := validator(value); !valid {
return false, reason
}
}
return true, ""
}
}
// 使用组合验证器
func TestCombinedValidators(t *testing.T) {
a := assert.New(t)
notEmpty := func(value interface{}) (bool, string) {
str, ok := value.(string)
if !ok || str == "" {
return false, "值不能为空"
}
return true, ""
}
minLength := func(min int) Validator {
return func(value interface{}) (bool, string) {
str, ok := value.(string)
if !ok || len(str) < min {
return false, fmt.Sprintf("长度至少%d", min)
}
return true, ""
}
}
// 组合多个验证器
validator := CombineValidators(notEmpty, minLength(3))
valid, reason := validator("hello")
a.True(valid, reason)
valid, reason = validator("hi")
a.False(valid)
a.Equal("长度至少3", reason)
}
---
04.断言扩展模式
a.流畅接口模式
创建流畅接口风格的断言扩展,支持链式调用。流畅接口让断言代码读起来更像自然语言,提高可读性。这种模式适合创建领域特定的断言库。
b.断言构建器
使用构建器模式创建复杂的断言。构建器积累多个断言条件,最后统一执行验证。这种模式适合需要组合多个条件的复杂验证场景。
c.代码示例
---
// 断言扩展模式
package extension
import (
"testing"
"github.com/stretchr/testify/assert"
)
// 流畅接口断言
type UserAssertion struct {
t *testing.T
user *User
}
func AssertUser(t *testing.T, user *User) *UserAssertion {
return &UserAssertion{t: t, user: user}
}
func (ua *UserAssertion) IsNotNil() *UserAssertion {
ua.t.Helper()
assert.NotNil(ua.t, ua.user)
return ua
}
func (ua *UserAssertion) HasName(name string) *UserAssertion {
ua.t.Helper()
assert.Equal(ua.t, name, ua.user.Name)
return ua
}
func (ua *UserAssertion) HasAge(age int) *UserAssertion {
ua.t.Helper()
assert.Equal(ua.t, age, ua.user.Age)
return ua
}
func (ua *UserAssertion) IsAdult() *UserAssertion {
ua.t.Helper()
assert.GreaterOrEqual(ua.t, ua.user.Age, 18)
return ua
}
// 使用流畅接口
func TestFluentAssertions(t *testing.T) {
user := &User{Name: "Alice", Age: 25, Email: "[email protected]"}
// 链式断言
AssertUser(t, user).
IsNotNil().
HasName("Alice").
HasAge(25).
IsAdult()
}
// 断言构建器
type AssertionBuilder struct {
t *testing.T
conditions []func() bool
messages []string
}
func NewAssertionBuilder(t *testing.T) *AssertionBuilder {
return &AssertionBuilder{t: t}
}
func (ab *AssertionBuilder) Add(condition func() bool, message string) *AssertionBuilder {
ab.conditions = append(ab.conditions, condition)
ab.messages = append(ab.messages, message)
return ab
}
func (ab *AssertionBuilder) Assert() {
ab.t.Helper()
for i, condition := range ab.conditions {
assert.True(ab.t, condition(), ab.messages[i])
}
}
// 使用断言构建器
func TestAssertionBuilder(t *testing.T) {
user := &User{Name: "Alice", Age: 25, Email: "[email protected]"}
NewAssertionBuilder(t).
Add(func() bool { return user.Name != "" }, "用户名不能为空").
Add(func() bool { return user.Age >= 18 }, "必须是成年人").
Add(func() bool { return strings.Contains(user.Email, "@") }, "邮箱格式有效").
Assert()
}
type User struct {
Name string
Age int
Email string
}
---
3.8 断言消息格式化
01.自定义错误消息
a.消息参数
testify的所有断言方法都支持可选的消息参数,用于在断言失败时提供额外的上下文信息。消息参数可以是简单字符串,也可以是格式化字符串配合多个参数,类似fmt.Printf的用法。清晰的错误消息能大幅提高测试失败时的诊断效率。
b.消息格式化语法
消息参数使用fmt包的格式化语法。第一个消息参数是格式字符串,后续参数是要插入的值。支持%s字符串、%d整数、%v通用格式、%+v详细格式等格式化动词。
c.代码示例
---
// 断言消息格式化详解
package message
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSimpleMessages(t *testing.T) {
a := assert.New(t)
// 简单字符串消息
a.Equal(5, Add(2, 3), "加法运算应该正确")
// 无消息(使用默认错误信息)
a.NotNil(GetUser(1))
// 空字符串消息
a.True(IsValid(), "")
}
func TestFormattedMessages(t *testing.T) {
a := assert.New(t)
// 格式化消息:%s - 字符串
username := "Alice"
a.NotEmpty(username, "用户名%s不能为空", username)
// 格式化消息:%d - 整数
age := 25
a.GreaterOrEqual(age, 18, "年龄%d必须大于等于18", age)
// 格式化消息:%v - 通用格式
user := User{Name: "Alice", Age: 25}
a.NotNil(user, "用户对象%v不能为nil", user)
// 格式化消息:%+v - 详细格式(包含字段名)
a.Equal("Alice", user.Name, "用户对象%+v的名称不匹配", user)
// 格式化消息:%T - 类型
var data interface{} = "hello"
a.IsType("", data, "期望类型string但得到%T", data)
}
func TestMultipleParameters(t *testing.T) {
a := assert.New(t)
// 多个参数
x, y := 10, 20
a.Less(x, y, "期望%d < %d但实际不是", x, y)
// 复杂消息
order := Order{ID: 123, Total: 99.99}
expected := 99.99
a.Equal(expected, order.Total,
"订单ID=%d的总额应该是%.2f,但实际是%.2f",
order.ID, expected, order.Total)
}
func TestContextualMessages(t *testing.T) {
a := assert.New(t)
// 场景1:循环中的断言
numbers := []int{2, 4, 6, 8, 10}
for i, n := range numbers {
a.Equal(0, n%2, "索引%d的元素%d应该是偶数", i, n)
}
// 场景2:测试数据上下文
testCases := []struct {
input int
expected int
}{
{1, 2},
{2, 4},
{3, 6},
}
for _, tc := range testCases {
result := Double(tc.input)
a.Equal(tc.expected, result,
"Double(%d)应该返回%d,但得到%d",
tc.input, tc.expected, result)
}
}
type User struct {
Name string
Age int
}
type Order struct {
ID int
Total float64
}
func Add(a, b int) int { return a + b }
func GetUser(id int) *User { return &User{Name: "Alice"} }
func IsValid() bool { return true }
func Double(n int) int { return n * 2 }
---
02.错误消息最佳实践
a.消息编写原则
错误消息应该清晰说明期望是什么,实际得到了什么,以及为什么这是错误的。包含足够的上下文信息帮助快速定位问题。使用主动语态,避免模糊的描述。消息应该面向开发者,使用技术术语。
b.消息内容指导
好的消息包含操作上下文、输入参数、期望结果、实际结果。避免冗余信息,testify已经显示了期望值和实际值。重点说明业务含义而不是重复断言本身的内容。
c.代码示例
---
// 错误消息最佳实践
package bestpractice
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGoodMessages(t *testing.T) {
a := assert.New(t)
// ✅ 好的消息:说明业务含义
age := 15
a.GreaterOrEqual(age, 18, "用户必须是成年人才能注册")
// ✅ 好的消息:包含上下文
userID := 123
user := GetUser(userID)
a.NotNil(user, "数据库应该包含ID为%d的用户", userID)
// ✅ 好的消息:说明影响
balance := -100.0
a.GreaterOrEqual(balance, 0.0, "余额不足,无法完成交易")
// ✅ 好的消息:指出问题
config := LoadConfig()
a.NotEmpty(config.DatabaseURL, "配置文件缺少必需的database_url字段")
}
func TestBadMessages(t *testing.T) {
a := assert.New(t)
// ❌ 差的消息:过于简单
age := 15
// a.GreaterOrEqual(age, 18, "错误")
// ❌ 差的消息:重复断言内容
// a.Equal(5, result, "result应该等于5")
// ❌ 差的消息:没有上下文
// a.NotNil(user, "不能为nil")
// ❌ 差的消息:信息不足
// a.True(valid, "验证失败")
// 改进:提供具体信息
a.True(valid, "邮箱格式验证失败:必须包含@符号")
}
func TestMessagePatterns(t *testing.T) {
a := assert.New(t)
// 模式1:说明前置条件
db := ConnectDB()
a.NotNil(db, "数据库连接必须在测试前建立")
// 模式2:说明期望行为
orders := GetOrders(userID)
a.NotEmpty(orders, "活跃用户应该至少有一个订单")
// 模式3:说明数据要求
email := user.Email
a.Contains(email, "@", "用户邮箱必须是有效的邮箱地址")
// 模式4:说明业务规则
discount := CalculateDiscount(order)
a.LessOrEqual(discount, 100.0, "折扣不能超过订单总额")
}
type User struct{ Email string }
type Order struct{}
type Config struct{ DatabaseURL string }
var userID = 123
var valid = true
func GetUser(id int) *User { return &User{} }
func LoadConfig() Config { return Config{} }
func ConnectDB() interface{} { return &struct{}{} }
func GetOrders(userID int) []Order { return []Order{} }
func CalculateDiscount(order Order) float64 { return 10.0 }
---
03.国际化考虑
a.语言选择
错误消息应该使用团队的工作语言。中文团队使用中文消息,英文团队使用英文消息。保持一致性,不要混用多种语言。技术术语可以使用英文,但说明性文字使用工作语言。
b.消息模板
为常见的断言场景创建消息模板,确保消息风格统一。模板可以包含占位符,使用时填入具体值。这有助于保持团队代码风格的一致性。
c.代码示例
---
// 国际化和消息模板
package i18n
import (
"testing"
"github.com/stretchr/testify/assert"
)
// 消息模板常量
const (
MsgUserNotFound = "未找到ID为%d的用户"
MsgInvalidAge = "年龄%d无效,必须在%d到%d之间"
MsgDatabaseError = "数据库操作失败:%s"
MsgConfigMissing = "配置项%s缺失或为空"
MsgPermissionDenied = "用户%s没有%s权限"
)
func TestWithMessageTemplates(t *testing.T) {
a := assert.New(t)
// 使用模板
userID := 999
user := FindUser(userID)
a.NotNil(user, MsgUserNotFound, userID)
// 使用模板验证范围
age := 200
a.True(age >= 0 && age <= 150, MsgInvalidAge, age, 0, 150)
// 使用模板说明权限
username := "guest"
permission := "admin"
hasPermission := CheckPermission(username, permission)
a.True(hasPermission, MsgPermissionDenied, username, permission)
}
// 中文消息示例
func Test中文消息(t *testing.T) {
a := assert.New(t)
用户名 := "张三"
年龄 := 25
a.NotEmpty(用户名, "用户名不能为空")
a.GreaterOrEqual(年龄, 18, "用户%s的年龄%d必须大于等于18", 用户名, 年龄)
}
// 英文消息示例
func TestEnglishMessages(t *testing.T) {
a := assert.New(t)
username := "Alice"
age := 25
a.NotEmpty(username, "username cannot be empty")
a.GreaterOrEqual(age, 18, "user %s age %d must be at least 18", username, age)
}
func FindUser(id int) *User {
if id == 999 {
return nil
}
return &User{}
}
func CheckPermission(user, perm string) bool {
return user != "guest" || perm != "admin"
}
---
04.调试友好的消息
a.包含诊断信息
在错误消息中包含有助于调试的信息,如当前状态、相关变量值、执行路径。帮助开发者快速理解失败原因,减少调试时间。特别是在CI环境中,详细的错误消息至关重要。
b.分层次的信息
简单场景使用简短消息,复杂场景使用详细消息。对于嵌套对象,考虑使用%+v显示完整结构。对于集合,可以显示前几个元素作为样本。
c.代码示例
---
// 调试友好的消息
package debug
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDebugFriendlyMessages(t *testing.T) {
a := assert.New(t)
// 场景1:显示相关状态
order := Order{
ID: 123,
Status: "pending",
Items: []Item{{ID: 1}, {ID: 2}},
}
a.Equal("completed", order.Status,
"订单ID=%d应该已完成,当前状态=%s,包含%d个商品",
order.ID, order.Status, len(order.Items))
// 场景2:显示完整对象
user := User{
ID: 1,
Name: "Alice",
Email: "[email protected]",
Roles: []string{"user", "admin"},
}
a.Contains(user.Roles, "superadmin",
"用户应该有superadmin角色,用户信息:%+v", user)
// 场景3:显示集合样本
items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
a.Len(items, 5,
"期望5个元素,实际%d个,前3个: %v",
len(items), items[:min(3, len(items))])
}
func TestExecutionPath(t *testing.T) {
a := assert.New(t)
// 显示执行路径
input := 10
step1 := Validate(input)
step2 := Transform(step1)
result := Calculate(step2)
expected := 42
a.Equal(expected, result,
"计算流程:输入=%d -> 验证=%d -> 转换=%d -> 结果=%d(期望%d)",
input, step1, step2, result, expected)
}
func TestComparisonDetails(t *testing.T) {
a := assert.New(t)
expected := map[string]int{
"apples": 5,
"oranges": 3,
"bananas": 7,
}
actual := map[string]int{
"apples": 5,
"oranges": 4, // 不同
"bananas": 7,
}
a.Equal(expected, actual,
"库存不匹配\n期望: %v\n实际: %v",
expected, actual)
}
type Order struct {
ID int
Status string
Items []Item
}
type Item struct {
ID int
}
type User struct {
ID int
Name string
Email string
Roles []string
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func Validate(n int) int { return n }
func Transform(n int) int { return n * 2 }
func Calculate(n int) int { return n + 22 }
---
04.Mock系统详解
01.Mock概念 a.什么是Mock Mock是测试中用来模拟真实对象行为的替代品。在单元测试中,Mock对象可以替代依赖项如数据库、外部API、文件系统等,使测试更快速、可控且独立。testify的mock包提供了强大的Mock功能,支持方法调用记录、参数验证和返回值控制。 b.Mock的作用 Mock隔离被测代码与外部依赖,使测试专注于当前单元。通过Mock可以模拟难以复现的场景如网络错误、超时等。Mock还能验证方法调用次数、参数和顺序,确保代码按预期与依赖交互。使用Mock可以大幅提升测试速度,避免真实依赖的开销。 c.代码示例 ``` // Mock基础概念示例 package mockbasic
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// 定义接口
type UserRepository interface {
GetByID(id int) (*User, error)
Save(user *User) error
Delete(id int) error
}
// 创建Mock对象
type MockUserRepository struct {
mock.Mock
}
// 实现接口方法
func (m *MockUserRepository) GetByID(id int) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func (m *MockUserRepository) Save(user *User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockUserRepository) Delete(id int) error {
args := m.Called(id)
return args.Error(0)
}
// 业务逻辑
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.GetByID(id)
}
func (s *UserService) CreateUser(name string) error {
user := &User{Name: name}
return s.repo.Save(user)
}
// 测试
func TestUserService_GetUser(t *testing.T) {
// 创建Mock
mockRepo := new(MockUserRepository)
// 设置期望
expectedUser := &User{ID: 1, Name: "Alice"}
mockRepo.On("GetByID", 1).Return(expectedUser, nil)
// 创建服务
service := NewUserService(mockRepo)
// 执行
user, err := service.GetUser(1)
// 断言
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)
// 验证Mock调用
mockRepo.AssertExpectations(t)
}
func TestUserService_CreateUser(t *testing.T) {
mockRepo := new(MockUserRepository)
// 设置期望:Save方法被调用一次,返回nil(成功)
mockRepo.On("Save", mock.Anything).Return(nil)
service := NewUserService(mockRepo)
err := service.CreateUser("Bob")
assert.NoError(t, err)
mockRepo.AssertExpectations(t)
}
type User struct {
ID int
Name string
}
```
02.Mock对象创建 a.嵌入mock.Mock 创建Mock对象的第一步是定义一个结构体,嵌入mock.Mock类型。这为结构体提供了Mock的核心功能如Called、On、AssertExpectations等方法。mock.Mock必须通过组合方式使用,不能直接实例化。 b.实现接口方法 Mock结构体需要实现目标接口的所有方法。每个方法内部调用m.Called()传入参数,然后从返回的Arguments中提取结果。testify会记录所有调用并与预设的期望进行匹配。 c.代码示例 ``` // Mock对象创建详解 package mockcreation
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// 场景1:简单接口Mock
type Calculator interface {
Add(a, b int) int
Divide(a, b int) (int, error)
}
type MockCalculator struct {
mock.Mock
}
func (m *MockCalculator) Add(a, b int) int {
args := m.Called(a, b)
return args.Int(0)
}
func (m *MockCalculator) Divide(a, b int) (int, error) {
args := m.Called(a, b)
return args.Int(0), args.Error(1)
}
// 场景2:复杂返回值Mock
type DataService interface {
Query(ctx context.Context, sql string, params []interface{}) ([]map[string]interface{}, error)
Execute(ctx context.Context, sql string) (affected int64, err error)
}
type MockDataService struct {
mock.Mock
}
func (m *MockDataService) Query(ctx context.Context, sql string, params []interface{}) ([]map[string]interface{}, error) {
args := m.Called(ctx, sql, params)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]map[string]interface{}), args.Error(1)
}
func (m *MockDataService) Execute(ctx context.Context, sql string) (int64, error) {
args := m.Called(ctx, sql)
return args.Get(0).(int64), args.Error(1)
}
// 场景3:可变参数Mock
type Logger interface {
Log(level string, msg string, fields ...interface{})
Logf(level string, format string, args ...interface{})
}
type MockLogger struct {
mock.Mock
}
func (m *MockLogger) Log(level string, msg string, fields ...interface{}) {
args := []interface{}{level, msg}
args = append(args, fields...)
m.Called(args...)
}
func (m *MockLogger) Logf(level string, format string, args ...interface{}) {
callArgs := []interface{}{level, format}
callArgs = append(callArgs, args...)
m.Called(callArgs...)
}
// 测试示例
func TestMockCreation(t *testing.T) {
// 测试简单Mock
calc := new(MockCalculator)
calc.On("Add", 2, 3).Return(5)
result := calc.Add(2, 3)
assert.Equal(t, 5, result)
calc.AssertExpectations(t)
// 测试复杂返回值Mock
ds := new(MockDataService)
ctx := context.Background()
expectedRows := []map[string]interface{}{
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
}
ds.On("Query", ctx, "SELECT * FROM users", []interface{}{}).
Return(expectedRows, nil)
rows, err := ds.Query(ctx, "SELECT * FROM users", []interface{}{})
assert.NoError(t, err)
assert.Equal(t, expectedRows, rows)
ds.AssertExpectations(t)
}
```
03.Mock生命周期 a.创建和初始化 使用new()或&MockType{}创建Mock对象实例。创建后立即使用On方法设置期望。一个Mock对象可以在同一个测试中设置多个期望,支持不同的方法调用。 b.使用和验证 将Mock对象注入到被测代码中。代码执行时,Mock会记录所有方法调用。测试结束时调用AssertExpectations验证所有期望的方法都被正确调用。如果有未满足的期望或意外的调用,测试会失败。 c.代码示例 ``` // Mock生命周期管理 package mocklifecycle
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type EmailService interface {
Send(to, subject, body string) error
SendBatch(recipients []string, subject, body string) error
}
type MockEmailService struct {
mock.Mock
}
func (m *MockEmailService) Send(to, subject, body string) error {
args := m.Called(to, subject, body)
return args.Error(0)
}
func (m *MockEmailService) SendBatch(recipients []string, subject, body string) error {
args := m.Called(recipients, subject, body)
return args.Error(0)
}
type NotificationService struct {
emailService EmailService
}
func (s *NotificationService) NotifyUser(email, message string) error {
return s.emailService.Send(email, "Notification", message)
}
func (s *NotificationService) NotifyAll(emails []string, message string) error {
return s.emailService.SendBatch(emails, "Broadcast", message)
}
func TestMockLifecycle(t *testing.T) {
// 1. 创建阶段
emailMock := new(MockEmailService)
// 2. 设置期望阶段
emailMock.On("Send", "[email protected]", "Notification", "Hello").
Return(nil)
// 3. 注入阶段
service := &NotificationService{emailService: emailMock}
// 4. 执行阶段
err := service.NotifyUser("[email protected]", "Hello")
// 5. 断言阶段
assert.NoError(t, err)
// 6. 验证期望阶段
emailMock.AssertExpectations(t)
}
func TestMultipleExpectations(t *testing.T) {
emailMock := new(MockEmailService)
// 设置多个期望
emailMock.On("Send", "[email protected]", "Notification", "Hi Alice").
Return(nil)
emailMock.On("Send", "[email protected]", "Notification", "Hi Bob").
Return(nil)
service := &NotificationService{emailService: emailMock}
// 多次调用
err1 := service.NotifyUser("[email protected]", "Hi Alice")
err2 := service.NotifyUser("[email protected]", "Hi Bob")
assert.NoError(t, err1)
assert.NoError(t, err2)
// 验证所有期望
emailMock.AssertExpectations(t)
}
func TestMockReuse(t *testing.T) {
// 注意:不建议在多个测试间共享Mock对象
// 每个测试应该创建自己的Mock
t.Run("Test1", func(t *testing.T) {
emailMock := new(MockEmailService)
emailMock.On("Send", mock.Anything, mock.Anything, mock.Anything).
Return(nil).
Once()
service := &NotificationService{emailService: emailMock}
err := service.NotifyUser("[email protected]", "Test")
assert.NoError(t, err)
emailMock.AssertExpectations(t)
})
t.Run("Test2", func(t *testing.T) {
// 创建新的Mock实例
emailMock := new(MockEmailService)
emailMock.On("SendBatch", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
service := &NotificationService{emailService: emailMock}
err := service.NotifyAll([]string{"[email protected]", "[email protected]"}, "Batch")
assert.NoError(t, err)
emailMock.AssertExpectations(t)
})
}
```
01.On方法基础 a.On方法语法 On方法用于设置Mock对象的期望行为。基本语法是mock.On(“方法名”, 参数列表).Return(返回值列表)。参数列表必须与实际调用时的参数类型和数量匹配。On方法返回一个Call对象,可以链式调用其他方法如Return、Times、Once等进行更详细的配置。 b.参数匹配规则 On方法的参数支持精确匹配和模糊匹配。精确匹配要求参数值完全相同。模糊匹配使用mock.Anything、mock.AnythingOfType等匹配器。多个On调用可以针对同一方法设置不同参数的期望,testify会根据实际调用选择匹配的期望。 c.代码示例 ``` // 期望设置基础 package expectation
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type StorageService interface {
Get(key string) (string, error)
Set(key, value string) error
Delete(key string) error
Exists(key string) bool
}
type MockStorageService struct {
mock.Mock
}
func (m *MockStorageService) Get(key string) (string, error) {
args := m.Called(key)
return args.String(0), args.Error(1)
}
func (m *MockStorageService) Set(key, value string) error {
args := m.Called(key, value)
return args.Error(0)
}
func (m *MockStorageService) Delete(key string) error {
args := m.Called(key)
return args.Error(0)
}
func (m *MockStorageService) Exists(key string) bool {
args := m.Called(key)
return args.Bool(0)
}
func TestBasicExpectation(t *testing.T) {
storage := new(MockStorageService)
// 基本期望设置
storage.On("Get", "username").Return("Alice", nil)
storage.On("Set", "username", "Bob").Return(nil)
storage.On("Delete", "oldkey").Return(nil)
storage.On("Exists", "config").Return(true)
// 执行并验证
value, err := storage.Get("username")
assert.NoError(t, err)
assert.Equal(t, "Alice", value)
err = storage.Set("username", "Bob")
assert.NoError(t, err)
err = storage.Delete("oldkey")
assert.NoError(t, err)
exists := storage.Exists("config")
assert.True(t, exists)
storage.AssertExpectations(t)
}
func TestMultipleExpectations(t *testing.T) {
storage := new(MockStorageService)
// 同一方法,不同参数的多个期望
storage.On("Get", "user:1").Return("Alice", nil)
storage.On("Get", "user:2").Return("Bob", nil)
storage.On("Get", "user:3").Return("", nil)
// testify会根据参数自动匹配
val1, _ := storage.Get("user:1")
val2, _ := storage.Get("user:2")
val3, _ := storage.Get("user:3")
assert.Equal(t, "Alice", val1)
assert.Equal(t, "Bob", val2)
assert.Equal(t, "", val3)
storage.AssertExpectations(t)
}
func TestAnythingMatcher(t *testing.T) {
storage := new(MockStorageService)
// 使用mock.Anything匹配任意参数
storage.On("Set", mock.Anything, mock.Anything).Return(nil)
storage.On("Get", mock.Anything).Return("default", nil)
// 任何参数都会匹配
err := storage.Set("key1", "value1")
assert.NoError(t, err)
err = storage.Set("key2", "value2")
assert.NoError(t, err)
value, _ := storage.Get("anykey")
assert.Equal(t, "default", value)
}
func TestExpectationPriority(t *testing.T) {
storage := new(MockStorageService)
// 精确匹配优先于模糊匹配
storage.On("Get", "special").Return("SPECIAL", nil)
storage.On("Get", mock.Anything).Return("default", nil)
// "special"匹配精确期望
special, _ := storage.Get("special")
assert.Equal(t, "SPECIAL", special)
// 其他键匹配模糊期望
other, _ := storage.Get("other")
assert.Equal(t, "default", other)
storage.AssertExpectations(t)
}
```
02.Return方法详解 a.返回单个值 Return方法指定Mock方法被调用时的返回值。参数数量和类型必须与接口方法的返回值匹配。对于单返回值方法,传入一个参数。对于多返回值方法,传入多个参数。nil可以用于指针和error类型。 b.返回多个值 Go方法常返回多个值,最后一个通常是error。Return方法按顺序接收所有返回值。例如Return(“result”, nil)表示返回字符串”result”和nil错误。返回值类型必须与接口定义完全匹配。 c.代码示例 ``` // Return方法详解 package returnmethod
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type DataService interface {
FetchOne(id int) (*Record, error)
FetchMany(ids []int) ([]*Record, error)
Count() int
Process(data []byte) (result []byte, count int, err error)
}
type MockDataService struct {
mock.Mock
}
func (m *MockDataService) FetchOne(id int) (*Record, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*Record), args.Error(1)
}
func (m *MockDataService) FetchMany(ids []int) ([]*Record, error) {
args := m.Called(ids)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*Record), args.Error(1)
}
func (m *MockDataService) Count() int {
args := m.Called()
return args.Int(0)
}
func (m *MockDataService) Process(data []byte) ([]byte, int, error) {
args := m.Called(data)
return args.Get(0).([]byte), args.Int(1), args.Error(2)
}
type Record struct {
ID int
Name string
}
func TestReturnSingleValue(t *testing.T) {
ds := new(MockDataService)
// 返回单个值
ds.On("Count").Return(42)
count := ds.Count()
assert.Equal(t, 42, count)
ds.AssertExpectations(t)
}
func TestReturnTwoValues(t *testing.T) {
ds := new(MockDataService)
// 返回两个值:对象和error
record := &Record{ID: 1, Name: "Alice"}
ds.On("FetchOne", 1).Return(record, nil)
// 返回nil和error
ds.On("FetchOne", 999).Return(nil, errors.New("not found"))
// 测试成功场景
r, err := ds.FetchOne(1)
assert.NoError(t, err)
assert.Equal(t, record, r)
// 测试错误场景
r, err = ds.FetchOne(999)
assert.Error(t, err)
assert.Nil(t, r)
ds.AssertExpectations(t)
}
func TestReturnMultipleValues(t *testing.T) {
ds := new(MockDataService)
// 返回三个值
input := []byte("input")
output := []byte("output")
ds.On("Process", input).Return(output, 10, nil)
result, count, err := ds.Process(input)
assert.NoError(t, err)
assert.Equal(t, output, result)
assert.Equal(t, 10, count)
ds.AssertExpectations(t)
}
func TestReturnSliceAndError(t *testing.T) {
ds := new(MockDataService)
// 返回切片
records := []*Record{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}
ds.On("FetchMany", []int{1, 2}).Return(records, nil)
// 返回空切片
ds.On("FetchMany", []int{}).Return([]*Record{}, nil)
// 返回nil切片和error
ds.On("FetchMany", []int{999}).Return(nil, errors.New("error"))
// 测试
r1, err1 := ds.FetchMany([]int{1, 2})
assert.NoError(t, err1)
assert.Len(t, r1, 2)
r2, err2 := ds.FetchMany([]int{})
assert.NoError(t, err2)
assert.Len(t, r2, 0)
r3, err3 := ds.FetchMany([]int{999})
assert.Error(t, err3)
assert.Nil(t, r3)
ds.AssertExpectations(t)
}
```
03.调用次数控制 a.Once和Times Once()限制方法只能被调用一次。Times(n)限制方法必须被调用n次。如果不指定次数,默认期望至少被调用一次。这些方法用于精确控制和验证方法调用频率,确保代码按预期执行。 b.Maybe和其他选项 Maybe()表示方法可能被调用也可能不被调用,不会导致测试失败。这适用于可选的操作如日志记录。还可以使用AtLeast、AtMost等方法设置范围。 c.代码示例 ``` // 调用次数控制 package callcount
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type CacheService interface {
Get(key string) (string, bool)
Set(key, value string)
Invalidate(key string)
Clear()
}
type MockCacheService struct {
mock.Mock
}
func (m *MockCacheService) Get(key string) (string, bool) {
args := m.Called(key)
return args.String(0), args.Bool(1)
}
func (m *MockCacheService) Set(key, value string) {
m.Called(key, value)
}
func (m *MockCacheService) Invalidate(key string) {
m.Called(key)
}
func (m *MockCacheService) Clear() {
m.Called()
}
func TestOnce(t *testing.T) {
cache := new(MockCacheService)
// 期望被调用一次
cache.On("Get", "config").Return("value", true).Once()
// 第一次调用成功
value, ok := cache.Get("config")
assert.True(t, ok)
assert.Equal(t, "value", value)
// 如果再次调用会失败(因为只期望一次)
// cache.Get("config") // 这会导致测试失败
cache.AssertExpectations(t)
}
func TestTimes(t *testing.T) {
cache := new(MockCacheService)
// 期望被调用3次
cache.On("Set", mock.Anything, mock.Anything).Times(3)
cache.Set("key1", "value1")
cache.Set("key2", "value2")
cache.Set("key3", "value3")
// 正好3次,验证通过
cache.AssertExpectations(t)
}
func TestMaybe(t *testing.T) {
cache := new(MockCacheService)
// 可能被调用也可能不被调用
cache.On("Clear").Maybe().Return()
cache.On("Get", "optional").Maybe().Return("", false)
// 不调用也不会失败
cache.AssertExpectations(t)
}
func TestMaybeWithCall(t *testing.T) {
cache := new(MockCacheService)
cache.On("Invalidate", "temp").Maybe().Return()
// 调用了也不会失败
cache.Invalidate("temp")
cache.AssertExpectations(t)
}
func TestMultipleCallsPattern(t *testing.T) {
cache := new(MockCacheService)
// 设置会被调用多次的期望
cache.On("Get", "user:123").Return("Alice", true).Times(3)
cache.On("Set", "user:123", mock.Anything).Once()
// 第一次Get
value1, _ := cache.Get("user:123")
assert.Equal(t, "Alice", value1)
// 更新
cache.Set("user:123", "Alice Updated")
// 再次Get(2次)
value2, _ := cache.Get("user:123")
value3, _ := cache.Get("user:123")
assert.Equal(t, "Alice", value2)
assert.Equal(t, "Alice", value3)
cache.AssertExpectations(t)
}
func TestUnlimitedCalls(t *testing.T) {
cache := new(MockCacheService)
// 不限制调用次数(至少一次)
cache.On("Get", "frequent").Return("value", true)
// 可以调用任意多次
for i := 0; i < 100; i++ {
cache.Get("frequent")
}
cache.AssertExpectations(t)
}
```
04.条件期望 a.运行时函数 可以使用Run方法在Mock方法被调用时执行自定义逻辑。这对于模拟副作用、验证参数、动态修改状态等场景很有用。Run接收一个函数,函数参数是Arguments对象,可以从中提取调用参数。 b.Return函数 ReturnFunc允许根据输入参数动态计算返回值。不同于固定的Return,ReturnFunc在每次调用时都会执行,可以根据参数返回不同的结果。这对于模拟复杂的业务逻辑很有帮助。 c.代码示例 ``` // 条件期望和动态行为 package conditional
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type TransformService interface {
Transform(input string) (string, error)
Validate(data interface{}) bool
Process(items []string) []string
}
type MockTransformService struct {
mock.Mock
}
func (m *MockTransformService) Transform(input string) (string, error) {
args := m.Called(input)
return args.String(0), args.Error(1)
}
func (m *MockTransformService) Validate(data interface{}) bool {
args := m.Called(data)
return args.Bool(0)
}
func (m *MockTransformService) Process(items []string) []string {
args := m.Called(items)
return args.Get(0).([]string)
}
func TestRunMethod(t *testing.T) {
service := new(MockTransformService)
var capturedInput string
// 使用Run捕获参数
service.On("Transform", mock.Anything).
Run(func(args mock.Arguments) {
capturedInput = args.String(0)
fmt.Printf("Transform called with: %s\n", capturedInput)
}).
Return("result", nil)
service.Transform("test input")
assert.Equal(t, "test input", capturedInput)
service.AssertExpectations(t)
}
func TestReturnFunc(t *testing.T) {
service := new(MockTransformService)
// 根据输入动态返回结果
service.On("Transform", mock.Anything).
Return(func(input string) string {
return strings.ToUpper(input)
}, func(input string) error {
if input == "" {
return fmt.Errorf("empty input")
}
return nil
})
// 测试不同输入
result1, err1 := service.Transform("hello")
assert.NoError(t, err1)
assert.Equal(t, "HELLO", result1)
result2, err2 := service.Transform("world")
assert.NoError(t, err2)
assert.Equal(t, "WORLD", result2)
_, err3 := service.Transform("")
assert.Error(t, err3)
service.AssertExpectations(t)
}
func TestConditionalValidation(t *testing.T) {
service := new(MockTransformService)
// 根据参数类型返回不同结果
service.On("Validate", mock.Anything).
Return(func(data interface{}) bool {
switch v := data.(type) {
case string:
return v != ""
case int:
return v > 0
default:
return false
}
})
assert.True(t, service.Validate("hello"))
assert.False(t, service.Validate(""))
assert.True(t, service.Validate(10))
assert.False(t, service.Validate(-5))
service.AssertExpectations(t)
}
func TestComplexDynamicBehavior(t *testing.T) {
service := new(MockTransformService)
callCount := 0
// 每次调用返回不同结果
service.On("Process", mock.Anything).
Return(func(items []string) []string {
callCount++
// 第一次调用:转换为大写
if callCount == 1 {
result := make([]string, len(items))
for i, item := range items {
result[i] = strings.ToUpper(item)
}
return result
}
// 第二次调用:反转顺序
if callCount == 2 {
result := make([]string, len(items))
for i, item := range items {
result[len(items)-1-i] = item
}
return result
}
// 后续调用:返回原样
return items
})
input := []string{"a", "b", "c"}
result1 := service.Process(input)
assert.Equal(t, []string{"A", "B", "C"}, result1)
result2 := service.Process(input)
assert.Equal(t, []string{"c", "b", "a"}, result2)
result3 := service.Process(input)
assert.Equal(t, input, result3)
service.AssertExpectations(t)
}
```
01.精确匹配 a.值匹配原理 精确匹配要求Mock方法调用时的参数值与On方法中指定的参数值完全相同。对于基本类型如int、string、bool,使用==比较。对于指针和引用类型,比较的是地址而不是内容,这可能导致意外的不匹配。 b.类型匹配规则 参数类型必须严格匹配。即使值相同,类型不同也不会匹配。例如int(5)和int64(5)是不同的。对于接口类型,实际传入的具体类型必须一致。切片、映射等复合类型需要特别注意。 c.代码示例 ``` // 精确参数匹配 package exactmatch
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type Calculator interface {
Add(a, b int) int
Concat(s1, s2 string) string
Multiply(factors []int) int
UpdateUser(user *User) error
}
type MockCalculator struct {
mock.Mock
}
func (m *MockCalculator) Add(a, b int) int {
args := m.Called(a, b)
return args.Int(0)
}
func (m *MockCalculator) Concat(s1, s2 string) string {
args := m.Called(s1, s2)
return args.String(0)
}
func (m *MockCalculator) Multiply(factors []int) int {
args := m.Called(factors)
return args.Int(0)
}
func (m *MockCalculator) UpdateUser(user *User) error {
args := m.Called(user)
return args.Error(0)
}
type User struct {
ID int
Name string
}
func TestExactValueMatch(t *testing.T) {
calc := new(MockCalculator)
// 精确匹配基本类型
calc.On("Add", 2, 3).Return(5)
calc.On("Add", 10, 20).Return(30)
result1 := calc.Add(2, 3)
assert.Equal(t, 5, result1)
result2 := calc.Add(10, 20)
assert.Equal(t, 30, result2)
// 不同的参数不会匹配
// calc.Add(1, 1) // 会失败,没有匹配的期望
calc.AssertExpectations(t)
}
func TestStringMatch(t *testing.T) {
calc := new(MockCalculator)
// 字符串精确匹配
calc.On("Concat", "Hello", "World").Return("HelloWorld")
calc.On("Concat", "", "").Return("")
result1 := calc.Concat("Hello", "World")
assert.Equal(t, "HelloWorld", result1)
result2 := calc.Concat("", "")
assert.Equal(t, "", result2)
calc.AssertExpectations(t)
}
func TestSliceMatch(t *testing.T) {
calc := new(MockCalculator)
// 注意:切片按引用比较,不是按内容
// 需要使用相同的切片实例
factors := []int{2, 3, 4}
calc.On("Multiply", factors).Return(24)
result := calc.Multiply(factors)
assert.Equal(t, 24, result)
// 内容相同但是不同实例的切片不会匹配
// calc.Multiply([]int{2, 3, 4}) // 会失败
calc.AssertExpectations(t)
}
func TestPointerMatch(t *testing.T) {
calc := new(MockCalculator)
// 指针按地址匹配
user := &User{ID: 1, Name: "Alice"}
calc.On("UpdateUser", user).Return(nil)
// 使用相同的指针实例
err := calc.UpdateUser(user)
assert.NoError(t, err)
// 不同的指针实例即使内容相同也不匹配
// differentUser := &User{ID: 1, Name: "Alice"}
// calc.UpdateUser(differentUser) // 会失败
calc.AssertExpectations(t)
}
func TestMatchOrdering(t *testing.T) {
calc := new(MockCalculator)
// 参数顺序很重要
calc.On("Concat", "A", "B").Return("AB")
calc.On("Concat", "B", "A").Return("BA")
result1 := calc.Concat("A", "B")
result2 := calc.Concat("B", "A")
assert.Equal(t, "AB", result1)
assert.Equal(t, "BA", result2)
calc.AssertExpectations(t)
}
```
02.模糊匹配器 a.mock.Anything mock.Anything匹配任意类型和任意值的参数。它是最宽松的匹配器,适用于不关心具体参数值的场景。可以用于任何参数位置,与其他精确参数或匹配器混合使用。 b.mock.AnythingOfType mock.AnythingOfType按类型匹配参数。传入类型的字符串表示如”string”、“int”、“*User”。这比Anything更严格,确保类型正确但不关心具体值。类型名必须准确,包括指针星号和包路径。 c.代码示例 ``` // 模糊匹配器 package fuzzymatch
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type DataStore interface {
Save(key string, value interface{}) error
Load(key string) (interface{}, error)
Update(id int, data map[string]interface{}) error
Query(filter func(interface{}) bool) []interface{}
}
type MockDataStore struct {
mock.Mock
}
func (m *MockDataStore) Save(key string, value interface{}) error {
args := m.Called(key, value)
return args.Error(0)
}
func (m *MockDataStore) Load(key string) (interface{}, error) {
args := m.Called(key)
return args.Get(0), args.Error(1)
}
func (m *MockDataStore) Update(id int, data map[string]interface{}) error {
args := m.Called(id, data)
return args.Error(0)
}
func (m *MockDataStore) Query(filter func(interface{}) bool) []interface{} {
args := m.Called(filter)
return args.Get(0).([]interface{})
}
func TestMockAnything(t *testing.T) {
store := new(MockDataStore)
// 匹配任意参数
store.On("Save", mock.Anything, mock.Anything).Return(nil)
// 任何参数都会匹配
err1 := store.Save("key1", "string value")
err2 := store.Save("key2", 123)
err3 := store.Save("key3", []int{1, 2, 3})
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.NoError(t, err3)
store.AssertExpectations(t)
}
func TestMixedMatchers(t *testing.T) {
store := new(MockDataStore)
// 精确匹配第一个参数,模糊匹配第二个
store.On("Save", "user:123", mock.Anything).Return(nil)
// 第一个参数必须是"user:123",第二个可以是任意值
err1 := store.Save("user:123", "Alice")
err2 := store.Save("user:123", map[string]string{"name": "Alice"})
assert.NoError(t, err1)
assert.NoError(t, err2)
// 第一个参数不匹配会失败
// store.Save("user:456", "Bob") // 失败
store.AssertExpectations(t)
}
func TestAnythingOfType(t *testing.T) {
store := new(MockDataStore)
// 按类型匹配
store.On("Save", mock.AnythingOfType("string"), mock.AnythingOfType("string")).
Return(nil)
store.On("Update", mock.AnythingOfType("int"), mock.AnythingOfType("map[string]interface {}")).
Return(nil)
// 类型匹配成功
err1 := store.Save("key", "value")
assert.NoError(t, err1)
data := map[string]interface{}{"field": "value"}
err2 := store.Update(1, data)
assert.NoError(t, err2)
// 类型不匹配会失败
// store.Save("key", 123) // 失败,第二个参数不是string
store.AssertExpectations(t)
}
func TestPointerTypeMatch(t *testing.T) {
type Record struct {
Data string
}
type RecordStore interface {
Insert(r *Record) error
Fetch(id int) *Record
}
type MockRecordStore struct {
mock.Mock
}
func (m *MockRecordStore) Insert(r *Record) error {
args := m.Called(r)
return args.Error(0)
}
func (m *MockRecordStore) Fetch(id int) *Record {
args := m.Called(id)
if args.Get(0) == nil {
return nil
}
return args.Get(0).(*Record)
}
store := new(MockRecordStore)
// 匹配指针类型(注意类型字符串中的*)
store.On("Insert", mock.AnythingOfType("*fuzzymatch.Record")).Return(nil)
record := &Record{Data: "test"}
err := store.Insert(record)
assert.NoError(t, err)
store.AssertExpectations(t)
}
func TestComplexTypeMatch(t *testing.T) {
store := new(MockDataStore)
// 匹配函数类型
store.On("Query", mock.AnythingOfType("func(interface {}) bool")).
Return([]interface{}{1, 2, 3})
filter := func(v interface{}) bool {
return true
}
results := store.Query(filter)
assert.Len(t, results, 3)
store.AssertExpectations(t)
}
```
03.自定义匹配器 a.MatchedBy方法 mock.MatchedBy接收一个判断函数,返回bool表示参数是否匹配。这允许实现任意复杂的匹配逻辑。函数接收interface{}类型参数,需要进行类型断言后再判断。 b.匹配器组合 多个匹配器可以组合使用,每个参数位置可以使用不同的匹配策略。可以混合精确匹配、Anything、AnythingOfType和MatchedBy。testify会从左到右依次验证每个参数。 c.代码示例 ``` // 自定义匹配器 package custommatch
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type Validator interface {
ValidateEmail(email string) bool
ValidateAge(age int) bool
ValidateUser(user *User) error
ProcessData(data []byte) error
}
type MockValidator struct {
mock.Mock
}
func (m *MockValidator) ValidateEmail(email string) bool {
args := m.Called(email)
return args.Bool(0)
}
func (m *MockValidator) ValidateAge(age int) bool {
args := m.Called(age)
return args.Bool(0)
}
func (m *MockValidator) ValidateUser(user *User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockValidator) ProcessData(data []byte) error {
args := m.Called(data)
return args.Error(0)
}
type User struct {
Name string
Email string
Age int
}
func TestMatchedByString(t *testing.T) {
validator := new(MockValidator)
// 自定义字符串匹配:包含@符号
validator.On("ValidateEmail",
mock.MatchedBy(func(email interface{}) bool {
s, ok := email.(string)
return ok && strings.Contains(s, "@")
})).Return(true)
// 包含@的邮箱匹配成功
result1 := validator.ValidateEmail("[email protected]")
assert.True(t, result1)
result2 := validator.ValidateEmail("[email protected]")
assert.True(t, result2)
// 不包含@的不匹配
// validator.ValidateEmail("invalid") // 失败
validator.AssertExpectations(t)
}
func TestMatchedByInt(t *testing.T) {
validator := new(MockValidator)
// 自定义整数匹配:大于等于18
validator.On("ValidateAge",
mock.MatchedBy(func(age interface{}) bool {
n, ok := age.(int)
return ok && n >= 18
})).Return(true)
// 成年人年龄匹配成功
result1 := validator.ValidateAge(18)
result2 := validator.ValidateAge(25)
result3 := validator.ValidateAge(100)
assert.True(t, result1)
assert.True(t, result2)
assert.True(t, result3)
// 未成年不匹配
// validator.ValidateAge(17) // 失败
validator.AssertExpectations(t)
}
func TestMatchedByStruct(t *testing.T) {
validator := new(MockValidator)
// 自定义结构体匹配:Name不为空
validator.On("ValidateUser",
mock.MatchedBy(func(u interface{}) bool {
user, ok := u.(*User)
return ok && user != nil && user.Name != ""
})).Return(nil)
// 有效用户匹配成功
err1 := validator.ValidateUser(&User{Name: "Alice"})
err2 := validator.ValidateUser(&User{Name: "Bob", Age: 30})
assert.NoError(t, err1)
assert.NoError(t, err2)
// 无效用户不匹配
// validator.ValidateUser(&User{}) // 失败,Name为空
// validator.ValidateUser(nil) // 失败,nil指针
validator.AssertExpectations(t)
}
func TestComplexMatcher(t *testing.T) {
validator := new(MockValidator)
// 复杂条件:长度在10-100字节之间且包含特定前缀
validator.On("ProcessData",
mock.MatchedBy(func(d interface{}) bool {
data, ok := d.([]byte)
if !ok {
return false
}
return len(data) >= 10 &&
len(data) <= 100 &&
strings.HasPrefix(string(data), "VALID:")
})).Return(nil)
// 符合条件的数据
err1 := validator.ProcessData([]byte("VALID:1234567890"))
assert.NoError(t, err1)
// 不符合条件
// validator.ProcessData([]byte("short")) // 失败,太短
// validator.ProcessData([]byte("INVALID:1234567890")) // 失败,前缀错误
validator.AssertExpectations(t)
}
func TestMatcherCombination(t *testing.T) {
type MultiParam interface {
Process(id int, name string, data []byte) error
}
type MockMultiParam struct {
mock.Mock
}
func (m *MockMultiParam) Process(id int, name string, data []byte) error {
args := m.Called(id, name, data)
return args.Error(0)
}
service := new(MockMultiParam)
// 组合不同的匹配器
service.On("Process",
mock.MatchedBy(func(id interface{}) bool {
n, ok := id.(int)
return ok && n > 0 // ID必须为正数
}),
mock.AnythingOfType("string"), // Name必须是字符串
mock.MatchedBy(func(data interface{}) bool {
d, ok := data.([]byte)
return ok && len(d) > 0 // Data不能为空
}),
).Return(nil)
// 满足所有条件
err := service.Process(123, "test", []byte("data"))
assert.NoError(t, err)
service.AssertExpectations(t)
}
```
04.特殊场景匹配 a.nil值匹配 nil可以作为参数直接传入On方法进行精确匹配。对于接口类型,nil表示接口值为nil。对于指针类型,nil表示空指针。使用mock.Anything也会匹配nil值。 b.空值和零值 空字符串""、空切片[]Type{}、零值0等都可以精确匹配。注意空切片和nil切片在Go中是不同的,前者是非nil但长度为0的切片,后者是nil。这在匹配时需要区分。 c.代码示例 ``` // 特殊场景参数匹配 package specialmatch
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type SpecialService interface {
HandleNil(data *Data) error
HandleEmpty(items []string) int
HandleZero(count int) bool
HandleInterface(value interface{}) string
}
type MockSpecialService struct {
mock.Mock
}
func (m *MockSpecialService) HandleNil(data *Data) error {
args := m.Called(data)
return args.Error(0)
}
func (m *MockSpecialService) HandleEmpty(items []string) int {
args := m.Called(items)
return args.Int(0)
}
func (m *MockSpecialService) HandleZero(count int) bool {
args := m.Called(count)
return args.Bool(0)
}
func (m *MockSpecialService) HandleInterface(value interface{}) string {
args := m.Called(value)
return args.String(0)
}
type Data struct {
Value string
}
func TestNilPointerMatch(t *testing.T) {
service := new(MockSpecialService)
// 精确匹配nil指针
service.On("HandleNil", (*Data)(nil)).Return(nil)
// 使用nil调用
err := service.HandleNil(nil)
assert.NoError(t, err)
service.AssertExpectations(t)
}
func TestNilWithAnything(t *testing.T) {
service := new(MockSpecialService)
// mock.Anything也匹配nil
service.On("HandleNil", mock.Anything).Return(nil)
// nil匹配
err1 := service.HandleNil(nil)
assert.NoError(t, err1)
// 非nil也匹配
err2 := service.HandleNil(&Data{Value: "test"})
assert.NoError(t, err2)
service.AssertExpectations(t)
}
func TestEmptySliceMatch(t *testing.T) {
service := new(MockSpecialService)
// 注意:空切片和nil切片不同
emptySlice := []string{}
service.On("HandleEmpty", emptySlice).Return(0)
// 使用相同的空切片实例
count := service.HandleEmpty(emptySlice)
assert.Equal(t, 0, count)
service.AssertExpectations(t)
}
func TestNilSliceMatch(t *testing.T) {
service := new(MockSpecialService)
// nil切片匹配
var nilSlice []string
service.On("HandleEmpty", nilSlice).Return(-1)
count := service.HandleEmpty(nilSlice)
assert.Equal(t, -1, count)
service.AssertExpectations(t)
}
func TestZeroValueMatch(t *testing.T) {
service := new(MockSpecialService)
// 零值精确匹配
service.On("HandleZero", 0).Return(true)
service.On("HandleZero", mock.MatchedBy(func(n interface{}) bool {
num, ok := n.(int)
return ok && num > 0
})).Return(false)
// 零值匹配第一个期望
result1 := service.HandleZero(0)
assert.True(t, result1)
// 正数匹配第二个期望
result2 := service.HandleZero(5)
assert.False(t, result2)
service.AssertExpectations(t)
}
func TestInterfaceNilMatch(t *testing.T) {
service := new(MockSpecialService)
// 接口类型的nil
service.On("HandleInterface", nil).Return("nil value")
service.On("HandleInterface", mock.Anything).Return("non-nil value")
// nil匹配精确期望(精确匹配优先)
result1 := service.HandleInterface(nil)
assert.Equal(t, "nil value", result1)
// 非nil匹配Anything
result2 := service.HandleInterface("test")
assert.Equal(t, "non-nil value", result2)
service.AssertExpectations(t)
}
func TestEmptyStringMatch(t *testing.T) {
type StringService interface {
Process(s string) int
}
type MockStringService struct {
mock.Mock
}
func (m *MockStringService) Process(s string) int {
args := m.Called(s)
return args.Int(0)
}
service := new(MockStringService)
// 空字符串精确匹配
service.On("Process", "").Return(0)
service.On("Process", mock.Anything).Return(1)
// 空字符串匹配精确期望
result1 := service.Process("")
assert.Equal(t, 0, result1)
// 非空字符串匹配Anything
result2 := service.Process("hello")
assert.Equal(t, 1, result2)
service.AssertExpectations(t)
}
```
4.4 返回值控制
01.固定返回值
a.Return方法回顾
Return方法设置Mock方法的固定返回值。每次调用该方法都会返回相同的值。参数必须与接口方法的返回签名匹配。对于多返回值,按顺序传入所有返回值。这是最简单直接的返回值控制方式。
b.返回nil和零值
Go的零值如0、""、false、nil可以直接作为返回值。对于指针和error类型,nil表示空值和无错误。返回nil时要注意接收方的类型断言,避免nil pointer dereference。
c.代码示例
---
// 固定返回值控制
package fixedreturn
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type ProductService interface {
GetPrice(productID int) (float64, error)
GetStock(productID int) (int, error)
GetProduct(id int) (*Product, error)
ListProducts(category string) ([]*Product, error)
}
type MockProductService struct {
mock.Mock
}
func (m *MockProductService) GetPrice(productID int) (float64, error) {
args := m.Called(productID)
return args.Get(0).(float64), args.Error(1)
}
func (m *MockProductService) GetStock(productID int) (int, error) {
args := m.Called(productID)
return args.Int(0), args.Error(1)
}
func (m *MockProductService) GetProduct(id int) (*Product, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*Product), args.Error(1)
}
func (m *MockProductService) ListProducts(category string) ([]*Product, error) {
args := m.Called(category)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*Product), args.Error(1)
}
type Product struct {
ID int
Name string
Price float64
Category string
}
func TestFixedReturn(t *testing.T) {
service := new(MockProductService)
// 固定返回基本类型
service.On("GetPrice", 1).Return(99.99, nil)
service.On("GetStock", 1).Return(50, nil)
price, err := service.GetPrice(1)
assert.NoError(t, err)
assert.Equal(t, 99.99, price)
stock, err := service.GetStock(1)
assert.NoError(t, err)
assert.Equal(t, 50, stock)
service.AssertExpectations(t)
}
func TestReturnNil(t *testing.T) {
service := new(MockProductService)
// 返回nil对象和error
service.On("GetProduct", 999).Return(nil, errors.New("not found"))
product, err := service.GetProduct(999)
assert.Error(t, err)
assert.Nil(t, product)
assert.Equal(t, "not found", err.Error())
service.AssertExpectations(t)
}
func TestReturnStruct(t *testing.T) {
service := new(MockProductService)
// 返回结构体指针
expectedProduct := &Product{
ID: 1,
Name: "Laptop",
Price: 999.99,
Category: "Electronics",
}
service.On("GetProduct", 1).Return(expectedProduct, nil)
product, err := service.GetProduct(1)
assert.NoError(t, err)
assert.Equal(t, expectedProduct, product)
assert.Equal(t, "Laptop", product.Name)
service.AssertExpectations(t)
}
func TestReturnSlice(t *testing.T) {
service := new(MockProductService)
// 返回切片
products := []*Product{
{ID: 1, Name: "Product1", Category: "A"},
{ID: 2, Name: "Product2", Category: "A"},
}
service.On("ListProducts", "A").Return(products, nil)
// 返回空切片
service.On("ListProducts", "B").Return([]*Product{}, nil)
// 返回nil切片和error
service.On("ListProducts", "C").Return(nil, errors.New("error"))
result1, err1 := service.ListProducts("A")
assert.NoError(t, err1)
assert.Len(t, result1, 2)
result2, err2 := service.ListProducts("B")
assert.NoError(t, err2)
assert.Len(t, result2, 0)
result3, err3 := service.ListProducts("C")
assert.Error(t, err3)
assert.Nil(t, result3)
service.AssertExpectations(t)
}
func TestReturnZeroValues(t *testing.T) {
service := new(MockProductService)
// 返回零值
service.On("GetPrice", 0).Return(0.0, nil)
service.On("GetStock", 0).Return(0, nil)
price, _ := service.GetPrice(0)
assert.Equal(t, 0.0, price)
stock, _ := service.GetStock(0)
assert.Equal(t, 0, stock)
service.AssertExpectations(t)
}
---
02.动态返回值
a.Return函数
Return可以接收函数作为参数,实现动态计算返回值。函数的参数对应Mock方法的参数,返回值对应Mock方法的返回值。这允许根据输入参数动态生成不同的输出,适合复杂的业务逻辑模拟。
b.Run配合Return
Run方法在方法调用时执行副作用,可以与Return配合使用。Run先执行,然后Return返回值。Run可以用于验证参数、修改外部状态、记录调用等。两者结合实现既有副作用又有返回值的复杂场景。
c.代码示例
---
// 动态返回值控制
package dynamicreturn
import (
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type ComputeService interface {
Calculate(x, y int) int
Transform(input string) (string, error)
Generate(seed int) []int
Timestamp() int64
}
type MockComputeService struct {
mock.Mock
}
func (m *MockComputeService) Calculate(x, y int) int {
args := m.Called(x, y)
return args.Int(0)
}
func (m *MockComputeService) Transform(input string) (string, error) {
args := m.Called(input)
return args.String(0), args.Error(1)
}
func (m *MockComputeService) Generate(seed int) []int {
args := m.Called(seed)
return args.Get(0).([]int)
}
func (m *MockComputeService) Timestamp() int64 {
args := m.Called()
return args.Get(0).(int64)
}
func TestReturnFunction(t *testing.T) {
service := new(MockComputeService)
// 使用函数动态计算返回值
service.On("Calculate", mock.Anything, mock.Anything).
Return(func(x, y int) int {
return x + y
})
// 不同输入得到不同结果
result1 := service.Calculate(2, 3)
result2 := service.Calculate(10, 20)
result3 := service.Calculate(100, 200)
assert.Equal(t, 5, result1)
assert.Equal(t, 30, result2)
assert.Equal(t, 300, result3)
service.AssertExpectations(t)
}
func TestReturnMultipleValues(t *testing.T) {
service := new(MockComputeService)
// 函数返回多个值
service.On("Transform", mock.AnythingOfType("string")).
Return(
func(input string) string {
return strings.ToUpper(input)
},
func(input string) error {
if input == "" {
return fmt.Errorf("empty input")
}
return nil
},
)
result1, err1 := service.Transform("hello")
assert.NoError(t, err1)
assert.Equal(t, "HELLO", result1)
_, err2 := service.Transform("")
assert.Error(t, err2)
service.AssertExpectations(t)
}
func TestReturnWithSideEffect(t *testing.T) {
service := new(MockComputeService)
callLog := []string{}
// Run记录调用,Return返回值
service.On("Calculate", mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
x := args.Int(0)
y := args.Int(1)
callLog = append(callLog, fmt.Sprintf("Calculate(%d, %d)", x, y))
}).
Return(func(x, y int) int {
return x * y
})
service.Calculate(2, 3)
service.Calculate(4, 5)
assert.Len(t, callLog, 2)
assert.Equal(t, "Calculate(2, 3)", callLog[0])
assert.Equal(t, "Calculate(4, 5)", callLog[1])
service.AssertExpectations(t)
}
func TestReturnSequentialValues(t *testing.T) {
service := new(MockComputeService)
sequence := []int{1, 2, 3, 4, 5}
index := 0
// 每次调用返回序列中的下一个值
service.On("Generate", mock.Anything).
Return(func(seed int) []int {
if index >= len(sequence) {
return []int{}
}
result := sequence[index : index+1]
index++
return result
})
assert.Equal(t, []int{1}, service.Generate(0))
assert.Equal(t, []int{2}, service.Generate(0))
assert.Equal(t, []int{3}, service.Generate(0))
service.AssertExpectations(t)
}
func TestReturnTimestamp(t *testing.T) {
service := new(MockComputeService)
// 每次调用返回当前时间戳
service.On("Timestamp").
Return(func() int64 {
return time.Now().Unix()
})
ts1 := service.Timestamp()
time.Sleep(time.Millisecond * 100)
ts2 := service.Timestamp()
assert.True(t, ts2 >= ts1)
service.AssertExpectations(t)
}
---
03.条件返回
a.基于参数的条件返回
通过设置多个On期望,可以根据不同的参数返回不同的值。testify会按照参数匹配选择对应的期望。这是实现条件返回的最简单方式,适用于参数空间有限的场景。
b.复杂条件逻辑
使用Return函数可以实现任意复杂的条件逻辑。在函数内部可以使用if-else、switch等控制结构,根据参数或外部状态决定返回值。这提供了最大的灵活性。
c.代码示例
---
// 条件返回值控制
package conditionalreturn
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type DiscountService interface {
CalculateDiscount(amount float64, userType string) float64
GetShippingCost(weight float64, distance int) (float64, error)
ApplyPromo(code string, amount float64) (float64, error)
}
type MockDiscountService struct {
mock.Mock
}
func (m *MockDiscountService) CalculateDiscount(amount float64, userType string) float64 {
args := m.Called(amount, userType)
return args.Get(0).(float64)
}
func (m *MockDiscountService) GetShippingCost(weight float64, distance int) (float64, error) {
args := m.Called(weight, distance)
return args.Get(0).(float64), args.Error(1)
}
func (m *MockDiscountService) ApplyPromo(code string, amount float64) (float64, error) {
args := m.Called(code, amount)
return args.Get(0).(float64), args.Error(1)
}
func TestParameterBasedReturn(t *testing.T) {
service := new(MockDiscountService)
// 根据用户类型返回不同折扣
service.On("CalculateDiscount", mock.Anything, "vip").
Return(func(amount float64, userType string) float64 {
return amount * 0.8 // VIP 20%折扣
})
service.On("CalculateDiscount", mock.Anything, "regular").
Return(func(amount float64, userType string) float64 {
return amount * 0.95 // 普通用户5%折扣
})
service.On("CalculateDiscount", mock.Anything, "guest").
Return(func(amount float64, userType string) float64 {
return amount // 访客无折扣
})
vipPrice := service.CalculateDiscount(100, "vip")
regularPrice := service.CalculateDiscount(100, "regular")
guestPrice := service.CalculateDiscount(100, "guest")
assert.Equal(t, 80.0, vipPrice)
assert.Equal(t, 95.0, regularPrice)
assert.Equal(t, 100.0, guestPrice)
service.AssertExpectations(t)
}
func TestComplexConditionalReturn(t *testing.T) {
service := new(MockDiscountService)
// 复杂的运费计算逻辑
service.On("GetShippingCost", mock.Anything, mock.Anything).
Return(
func(weight float64, distance int) float64 {
// 基础运费
base := 10.0
// 重量附加费
if weight > 5.0 {
base += (weight - 5.0) * 2.0
}
// 距离附加费
if distance > 100 {
base += float64(distance-100) * 0.1
}
return base
},
func(weight float64, distance int) error {
if weight <= 0 || distance <= 0 {
return errors.New("invalid parameters")
}
if weight > 100 {
return errors.New("weight exceeds limit")
}
return nil
},
)
// 轻量近距离
cost1, err1 := service.GetShippingCost(3.0, 50)
assert.NoError(t, err1)
assert.Equal(t, 10.0, cost1)
// 重量远距离
cost2, err2 := service.GetShippingCost(10.0, 200)
assert.NoError(t, err2)
assert.Equal(t, 30.0, cost2) // 10 + (10-5)*2 + (200-100)*0.1
// 无效参数
_, err3 := service.GetShippingCost(0, 100)
assert.Error(t, err3)
// 超重
_, err4 := service.GetShippingCost(150, 50)
assert.Error(t, err4)
service.AssertExpectations(t)
}
func TestPromoCodeReturn(t *testing.T) {
service := new(MockDiscountService)
// 模拟促销码验证和折扣计算
validCodes := map[string]float64{
"SAVE10": 0.9,
"SAVE20": 0.8,
"SPECIAL": 0.7,
}
service.On("ApplyPromo", mock.AnythingOfType("string"), mock.Anything).
Return(
func(code string, amount float64) float64 {
if discount, ok := validCodes[code]; ok {
return amount * discount
}
return amount
},
func(code string, amount float64) error {
if _, ok := validCodes[code]; !ok {
return errors.New("invalid promo code")
}
if amount < 50 {
return errors.New("minimum amount not met")
}
return nil
},
)
// 有效促销码
price1, err1 := service.ApplyPromo("SAVE10", 100)
assert.NoError(t, err1)
assert.Equal(t, 90.0, price1)
// 无效促销码
_, err2 := service.ApplyPromo("INVALID", 100)
assert.Error(t, err2)
// 金额不足
_, err3 := service.ApplyPromo("SAVE10", 30)
assert.Error(t, err3)
service.AssertExpectations(t)
}
---
04.返回值序列
a.多次调用不同返回值
有时需要同一方法在不同调用时返回不同的值。可以使用多个On调用配合Once()来实现。第一次调用匹配第一个期望,第二次匹配第二个,依此类推。这对于模拟状态变化很有用。
b.状态机模拟
通过闭包捕获状态变量,可以在Return函数中实现状态机。每次调用根据当前状态返回值并更新状态。这能模拟复杂的有状态服务,如连接池、会话管理等。
c.代码示例
---
// 返回值序列和状态管理
package returnsequence
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type ConnectionService interface {
Connect() error
GetStatus() string
Read() (string, error)
}
type MockConnectionService struct {
mock.Mock
}
func (m *MockConnectionService) Connect() error {
args := m.Called()
return args.Error(0)
}
func (m *MockConnectionService) GetStatus() string {
args := m.Called()
return args.String(0)
}
func (m *MockConnectionService) Read() (string, error) {
args := m.Called()
return args.String(0), args.Error(1)
}
func TestSequentialReturns(t *testing.T) {
service := new(MockConnectionService)
// 第一次调用返回错误,第二次成功
service.On("Connect").Return(errors.New("connection failed")).Once()
service.On("Connect").Return(nil).Once()
// 第一次连接失败
err1 := service.Connect()
assert.Error(t, err1)
// 第二次连接成功
err2 := service.Connect()
assert.NoError(t, err2)
service.AssertExpectations(t)
}
func TestStatusStateMachine(t *testing.T) {
service := new(MockConnectionService)
// 模拟连接状态转换:disconnected -> connecting -> connected
states := []string{"disconnected", "connecting", "connected"}
currentState := 0
service.On("GetStatus").
Return(func() string {
if currentState < len(states) {
state := states[currentState]
currentState++
return state
}
return states[len(states)-1]
})
assert.Equal(t, "disconnected", service.GetStatus())
assert.Equal(t, "connecting", service.GetStatus())
assert.Equal(t, "connected", service.GetStatus())
assert.Equal(t, "connected", service.GetStatus()) // 保持在最后状态
service.AssertExpectations(t)
}
func TestDataStreamSimulation(t *testing.T) {
service := new(MockConnectionService)
// 模拟数据流:返回一系列数据,然后EOF
dataStream := []string{"data1", "data2", "data3"}
index := 0
service.On("Read").
Return(
func() string {
if index < len(dataStream) {
data := dataStream[index]
index++
return data
}
return ""
},
func() error {
if index > len(dataStream) {
return errors.New("EOF")
}
return nil
},
)
// 读取数据流
data1, err1 := service.Read()
assert.NoError(t, err1)
assert.Equal(t, "data1", data1)
data2, err2 := service.Read()
assert.NoError(t, err2)
assert.Equal(t, "data2", data2)
data3, err3 := service.Read()
assert.NoError(t, err3)
assert.Equal(t, "data3", data3)
// EOF
_, err4 := service.Read()
assert.Error(t, err4)
assert.Equal(t, "EOF", err4.Error())
service.AssertExpectations(t)
}
func TestRetryPattern(t *testing.T) {
type RetryService interface {
TryOperation() (bool, error)
}
type MockRetryService struct {
mock.Mock
}
func (m *MockRetryService) TryOperation() (bool, error) {
args := m.Called()
return args.Bool(0), args.Error(1)
}
service := new(MockRetryService)
// 模拟重试:前两次失败,第三次成功
attemptCount := 0
service.On("TryOperation").
Return(
func() bool {
attemptCount++
return attemptCount >= 3
},
func() error {
if attemptCount < 3 {
return errors.New("temporary failure")
}
return nil
},
)
// 第一次尝试
success1, err1 := service.TryOperation()
assert.False(t, success1)
assert.Error(t, err1)
// 第二次尝试
success2, err2 := service.TryOperation()
assert.False(t, success2)
assert.Error(t, err2)
// 第三次尝试成功
success3, err3 := service.TryOperation()
assert.True(t, success3)
assert.NoError(t, err3)
service.AssertExpectations(t)
}
---
4.5 调用验证
01.AssertExpectations基础
a.验证原理
AssertExpectations验证所有通过On方法设置的期望是否都被满足。它检查方法是否被调用、调用次数是否正确、参数是否匹配。如果有未满足的期望,测试会失败并报告详细信息。这是Mock验证的核心方法。
b.使用时机
通常在测试结束时调用AssertExpectations。它接收*testing.T参数,用于报告失败。如果忘记调用此方法,即使Mock期望未满足,测试也可能通过,导致假阳性。建议每个使用Mock的测试都调用此方法。
c.代码示例
---
// AssertExpectations基础
package assertbasic
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type NotificationService interface {
SendEmail(to, subject, body string) error
SendSMS(phone, message string) error
LogEvent(event string)
}
type MockNotificationService struct {
mock.Mock
}
func (m *MockNotificationService) SendEmail(to, subject, body string) error {
args := m.Called(to, subject, body)
return args.Error(0)
}
func (m *MockNotificationService) SendSMS(phone, message string) error {
args := m.Called(phone, message)
return args.Error(0)
}
func (m *MockNotificationService) LogEvent(event string) {
m.Called(event)
}
func TestAssertExpectationsSuccess(t *testing.T) {
service := new(MockNotificationService)
// 设置期望
service.On("SendEmail", "[email protected]", "Welcome", "Hello!").
Return(nil)
// 执行调用
err := service.SendEmail("[email protected]", "Welcome", "Hello!")
assert.NoError(t, err)
// 验证期望
service.AssertExpectations(t)
}
func TestAssertExpectationsMultiple(t *testing.T) {
service := new(MockNotificationService)
// 设置多个期望
service.On("SendEmail", mock.Anything, mock.Anything, mock.Anything).
Return(nil).Once()
service.On("SendSMS", mock.Anything, mock.Anything).
Return(nil).Once()
service.On("LogEvent", "notification_sent").Once()
// 执行所有调用
service.SendEmail("[email protected]", "Test", "Body")
service.SendSMS("1234567890", "Test SMS")
service.LogEvent("notification_sent")
// 所有期望都满足
service.AssertExpectations(t)
}
func TestAssertExpectationsWithTimes(t *testing.T) {
service := new(MockNotificationService)
// 期望被调用3次
service.On("LogEvent", "event").Times(3)
// 调用3次
service.LogEvent("event")
service.LogEvent("event")
service.LogEvent("event")
// 验证通过
service.AssertExpectations(t)
}
func TestAssertExpectationsMaybe(t *testing.T) {
service := new(MockNotificationService)
// Maybe期望:调用或不调用都不会失败
service.On("SendEmail", mock.Anything, mock.Anything, mock.Anything).
Return(nil).Maybe()
// 不调用也能通过验证
service.AssertExpectations(t)
}
func TestAssertExpectationsMaybeWithCall(t *testing.T) {
service := new(MockNotificationService)
service.On("LogEvent", "optional").Maybe()
// 调用了也能通过
service.LogEvent("optional")
service.AssertExpectations(t)
}
---
02.AssertCalled方法
a.方法功能
AssertCalled验证指定的方法是否被调用过,参数必须匹配。与AssertExpectations不同,它用于事后检查,不需要预先设置期望。适用于只关心方法是否被调用,不关心返回值的场景。
b.AssertNotCalled
AssertNotCalled验证指定的方法没有被调用。这对于测试某些代码路径不应执行特定操作很有用。例如验证在缓存命中时不应访问数据库。
c.代码示例
---
// AssertCalled和AssertNotCalled
package assertcalled
import (
"testing"
"github.com/stretchr/testify/mock"
)
type CacheService interface {
Get(key string) (string, bool)
Set(key, value string)
Delete(key string)
}
type MockCacheService struct {
mock.Mock
}
func (m *MockCacheService) Get(key string) (string, bool) {
args := m.Called(key)
return args.String(0), args.Bool(1)
}
func (m *MockCacheService) Set(key, value string) {
m.Called(key, value)
}
func (m *MockCacheService) Delete(key string) {
m.Called(key)
}
type DatabaseService interface {
Query(sql string) (string, error)
Execute(sql string) error
}
type MockDatabaseService struct {
mock.Mock
}
func (m *MockDatabaseService) Query(sql string) (string, error) {
args := m.Called(sql)
return args.String(0), args.Error(1)
}
func (m *MockDatabaseService) Execute(sql string) error {
args := m.Called(sql)
return args.Error(0)
}
func TestAssertCalled(t *testing.T) {
cache := new(MockCacheService)
// 不需要预设期望
cache.On("Get", "key1").Return("value1", true)
cache.On("Set", "key2", "value2").Return()
// 执行调用
cache.Get("key1")
cache.Set("key2", "value2")
// 验证方法被调用
cache.AssertCalled(t, "Get", "key1")
cache.AssertCalled(t, "Set", "key2", "value2")
}
func TestAssertNotCalled(t *testing.T) {
cache := new(MockCacheService)
db := new(MockDatabaseService)
// 模拟缓存命中场景
cache.On("Get", "user:123").Return("Alice", true)
db.On("Query", mock.Anything).Return("Alice", nil)
// 从缓存获取数据
value, hit := cache.Get("user:123")
if hit {
// 缓存命中,不查询数据库
_ = value
} else {
db.Query("SELECT name FROM users WHERE id=123")
}
// 验证数据库没有被调用
db.AssertNotCalled(t, "Query", mock.Anything)
// 验证缓存被调用
cache.AssertCalled(t, "Get", "user:123")
}
func TestAssertCalledTimes(t *testing.T) {
cache := new(MockCacheService)
cache.On("Set", mock.Anything, mock.Anything).Return()
// 调用3次
cache.Set("key1", "val1")
cache.Set("key2", "val2")
cache.Set("key3", "val3")
// 验证具体调用
cache.AssertCalled(t, "Set", "key1", "val1")
cache.AssertCalled(t, "Set", "key2", "val2")
cache.AssertCalled(t, "Set", "key3", "val3")
}
func TestConditionalCalls(t *testing.T) {
cache := new(MockCacheService)
db := new(MockDatabaseService)
// 设置期望
cache.On("Get", "config").Return("", false) // 缓存未命中
db.On("Query", "SELECT value FROM config WHERE key='config'").
Return("config_value", nil)
cache.On("Set", "config", "config_value").Return()
// 先查缓存
value, hit := cache.Get("config")
if !hit {
// 未命中,查数据库
value, _ = db.Query("SELECT value FROM config WHERE key='config'")
// 写入缓存
cache.Set("config", value)
}
// 验证调用链
cache.AssertCalled(t, "Get", "config")
db.AssertCalled(t, "Query", "SELECT value FROM config WHERE key='config'")
cache.AssertCalled(t, "Set", "config", "config_value")
}
---
03.NumberOfCalls验证
a.获取调用次数
NumberOfCalls返回指定方法被调用的次数。它接收方法名和参数,返回匹配该签名的调用次数。这允许更精细的调用次数验证,不会导致测试失败,只是返回数字。
b.AssertNumberOfCalls
AssertNumberOfCalls断言方法被调用了指定的次数。它比On().Times()更灵活,可以在调用后验证而不需要预先设置。适用于验证循环、批处理等场景的调用次数。
c.代码示例
---
// 调用次数验证
package callcount
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type LoggerService interface {
Debug(msg string)
Info(msg string)
Warn(msg string)
Error(msg string)
}
type MockLoggerService struct {
mock.Mock
}
func (m *MockLoggerService) Debug(msg string) {
m.Called(msg)
}
func (m *MockLoggerService) Info(msg string) {
m.Called(msg)
}
func (m *MockLoggerService) Warn(msg string) {
m.Called(msg)
}
func (m *MockLoggerService) Error(msg string) {
m.Called(msg)
}
func TestNumberOfCalls(t *testing.T) {
logger := new(MockLoggerService)
logger.On("Info", mock.Anything).Return()
logger.On("Error", mock.Anything).Return()
// 执行多次调用
logger.Info("Start")
logger.Info("Processing")
logger.Info("Done")
logger.Error("Failed")
// 获取调用次数
infoCount := logger.NumberOfCalls("Info", mock.Anything)
errorCount := logger.NumberOfCalls("Error", mock.Anything)
assert.Equal(t, 3, infoCount)
assert.Equal(t, 1, errorCount)
}
func TestAssertNumberOfCalls(t *testing.T) {
logger := new(MockLoggerService)
logger.On("Debug", mock.Anything).Return()
// 调用5次
for i := 0; i < 5; i++ {
logger.Debug("debug message")
}
// 断言调用次数
logger.AssertNumberOfCalls(t, "Debug", 5)
}
func TestBatchProcessing(t *testing.T) {
type BatchProcessor interface {
Process(item string) error
}
type MockBatchProcessor struct {
mock.Mock
}
func (m *MockBatchProcessor) Process(item string) error {
args := m.Called(item)
return args.Error(0)
}
processor := new(MockBatchProcessor)
processor.On("Process", mock.Anything).Return(nil)
// 批处理多个项目
items := []string{"item1", "item2", "item3", "item4", "item5"}
for _, item := range items {
processor.Process(item)
}
// 验证处理了所有项目
count := processor.NumberOfCalls("Process", mock.Anything)
assert.Equal(t, len(items), count)
logger.AssertNumberOfCalls(t, "Process", 5)
}
func TestConditionalLogging(t *testing.T) {
logger := new(MockLoggerService)
logger.On("Info", mock.Anything).Return()
logger.On("Warn", mock.Anything).Return()
logger.On("Error", mock.Anything).Return()
// 模拟不同级别的日志
errors := []string{"error1", "error2"}
warnings := []string{"warn1"}
infos := []string{"info1", "info2", "info3", "info4"}
for _, e := range errors {
logger.Error(e)
}
for _, w := range warnings {
logger.Warn(w)
}
for _, i := range infos {
logger.Info(i)
}
// 验证各级别的调用次数
logger.AssertNumberOfCalls(t, "Error", 2)
logger.AssertNumberOfCalls(t, "Warn", 1)
logger.AssertNumberOfCalls(t, "Info", 4)
// 总调用次数
totalCalls := logger.NumberOfCalls("Error", mock.Anything) +
logger.NumberOfCalls("Warn", mock.Anything) +
logger.NumberOfCalls("Info", mock.Anything)
assert.Equal(t, 7, totalCalls)
}
---
04.调用顺序验证
a.顺序的重要性
某些场景下方法调用顺序很重要,如初始化流程、事务操作、资源管理等。testify本身不直接支持顺序验证,但可以通过Run方法和外部计数器实现。验证顺序确保代码按预期的步骤执行。
b.实现顺序验证
使用Run方法捕获调用时间或序号,记录到外部变量。在测试结束时检查记录的顺序是否符合预期。这种方式虽然需要额外代码,但提供了完全的控制和灵活性。
c.代码示例
---
// 调用顺序验证
package callorder
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type ResourceManager interface {
Acquire() error
Use() error
Release() error
}
type MockResourceManager struct {
mock.Mock
}
func (m *MockResourceManager) Acquire() error {
args := m.Called()
return args.Error(0)
}
func (m *MockResourceManager) Use() error {
args := m.Called()
return args.Error(0)
}
func (m *MockResourceManager) Release() error {
args := m.Called()
return args.Error(0)
}
func TestCallOrder(t *testing.T) {
manager := new(MockResourceManager)
callOrder := []string{}
// 使用Run记录调用顺序
manager.On("Acquire").Run(func(args mock.Arguments) {
callOrder = append(callOrder, "Acquire")
}).Return(nil)
manager.On("Use").Run(func(args mock.Arguments) {
callOrder = append(callOrder, "Use")
}).Return(nil)
manager.On("Release").Run(func(args mock.Arguments) {
callOrder = append(callOrder, "Release")
}).Return(nil)
// 按顺序执行
manager.Acquire()
manager.Use()
manager.Release()
// 验证顺序
expectedOrder := []string{"Acquire", "Use", "Release"}
assert.Equal(t, expectedOrder, callOrder)
manager.AssertExpectations(t)
}
func TestTransactionOrder(t *testing.T) {
type TransactionManager interface {
Begin() error
Execute(sql string) error
Commit() error
Rollback() error
}
type MockTransactionManager struct {
mock.Mock
}
func (m *MockTransactionManager) Begin() error {
args := m.Called()
return args.Error(0)
}
func (m *MockTransactionManager) Execute(sql string) error {
args := m.Called(sql)
return args.Error(0)
}
func (m *MockTransactionManager) Commit() error {
args := m.Called()
return args.Error(0)
}
func (m *MockTransactionManager) Rollback() error {
args := m.Called()
return args.Error(0)
}
tm := new(MockTransactionManager)
sequence := 0
callSequence := make(map[string]int)
// 记录每个方法的调用序号
tm.On("Begin").Run(func(args mock.Arguments) {
sequence++
callSequence["Begin"] = sequence
}).Return(nil)
tm.On("Execute", mock.Anything).Run(func(args mock.Arguments) {
sequence++
callSequence["Execute"] = sequence
}).Return(nil)
tm.On("Commit").Run(func(args mock.Arguments) {
sequence++
callSequence["Commit"] = sequence
}).Return(nil)
// 执行事务
tm.Begin()
tm.Execute("INSERT INTO users VALUES(1, 'Alice')")
tm.Commit()
// 验证调用顺序
assert.Less(t, callSequence["Begin"], callSequence["Execute"])
assert.Less(t, callSequence["Execute"], callSequence["Commit"])
tm.AssertExpectations(t)
}
func TestInitializationOrder(t *testing.T) {
type Initializer interface {
LoadConfig() error
ConnectDB() error
StartServer() error
}
type MockInitializer struct {
mock.Mock
}
func (m *MockInitializer) LoadConfig() error {
args := m.Called()
return args.Error(0)
}
func (m *MockInitializer) ConnectDB() error {
args := m.Called()
return args.Error(0)
}
func (m *MockInitializer) StartServer() error {
args := m.Called()
return args.Error(0)
}
init := new(MockInitializer)
operations := []string{}
init.On("LoadConfig").Run(func(args mock.Arguments) {
operations = append(operations, "LoadConfig")
}).Return(nil)
init.On("ConnectDB").Run(func(args mock.Arguments) {
// 验证LoadConfig已经被调用
assert.Contains(t, operations, "LoadConfig",
"ConnectDB should be called after LoadConfig")
operations = append(operations, "ConnectDB")
}).Return(nil)
init.On("StartServer").Run(func(args mock.Arguments) {
// 验证前两步已完成
assert.Contains(t, operations, "LoadConfig")
assert.Contains(t, operations, "ConnectDB")
operations = append(operations, "StartServer")
}).Return(nil)
// 按正确顺序初始化
init.LoadConfig()
init.ConnectDB()
init.StartServer()
// 验证最终顺序
expected := []string{"LoadConfig", "ConnectDB", "StartServer"}
assert.Equal(t, expected, operations)
init.AssertExpectations(t)
}
---
4.6 Mock接口
01.接口设计原则
a.小接口优于大接口
设计接口时应遵循接口隔离原则,创建小而专注的接口而不是大而全的接口。小接口更容易Mock,测试更清晰,依赖更明确。Go的隐式接口实现使得可以为不同测试场景定义不同的小接口。
b.面向测试的接口
在设计生产代码时考虑可测试性。将依赖定义为接口而不是具体类型。接口应该只包含业务逻辑需要的方法。避免依赖难以Mock的类型如全局变量、单例、静态方法等。
c.代码示例
---
// 接口设计最佳实践
package interfacedesign
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// ❌ 错误:接口太大,包含过多方法
type BadUserService interface {
Create(user User) error
Update(user User) error
Delete(id int) error
GetByID(id int) (*User, error)
GetByEmail(email string) (*User, error)
List(offset, limit int) ([]*User, error)
Count() (int, error)
Authenticate(email, password string) (bool, error)
ResetPassword(email string) error
SendEmail(to, subject, body string) error
LogActivity(userID int, action string) error
}
// ✅ 正确:拆分成多个小接口
type UserRepository interface {
Create(user User) error
Update(user User) error
Delete(id int) error
GetByID(id int) (*User, error)
GetByEmail(email string) (*User, error)
}
type UserQueryService interface {
List(offset, limit int) ([]*User, error)
Count() (int, error)
}
type AuthService interface {
Authenticate(email, password string) (bool, error)
ResetPassword(email string) error
}
type NotificationService interface {
SendEmail(to, subject, body string) error
}
type ActivityLogger interface {
LogActivity(userID int, action string) error
}
// User结构体
type User struct {
ID int
Email string
Name string
}
// 业务逻辑:使用小接口组合
type UserManager struct {
repo UserRepository
auth AuthService
notify NotificationService
logger ActivityLogger
}
func NewUserManager(repo UserRepository, auth AuthService,
notify NotificationService, logger ActivityLogger) *UserManager {
return &UserManager{
repo: repo,
auth: auth,
notify: notify,
logger: logger,
}
}
func (m *UserManager) CreateUser(user User) error {
// 创建用户
if err := m.repo.Create(user); err != nil {
return err
}
// 发送欢迎邮件
m.notify.SendEmail(user.Email, "Welcome", "Welcome to our service!")
// 记录活动
m.logger.LogActivity(user.ID, "user_created")
return nil
}
// Mock实现
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) Create(user User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockUserRepository) Update(user User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockUserRepository) Delete(id int) error {
args := m.Called(id)
return args.Error(0)
}
func (m *MockUserRepository) GetByID(id int) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func (m *MockUserRepository) GetByEmail(email string) (*User, error) {
args := m.Called(email)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
type MockAuthService struct {
mock.Mock
}
func (m *MockAuthService) Authenticate(email, password string) (bool, error) {
args := m.Called(email, password)
return args.Bool(0), args.Error(1)
}
func (m *MockAuthService) ResetPassword(email string) error {
args := m.Called(email)
return args.Error(0)
}
type MockNotificationService struct {
mock.Mock
}
func (m *MockNotificationService) SendEmail(to, subject, body string) error {
args := m.Called(to, subject, body)
return args.Error(0)
}
type MockActivityLogger struct {
mock.Mock
}
func (m *MockActivityLogger) LogActivity(userID int, action string) error {
args := m.Called(userID, action)
return args.Error(0)
}
// 测试:只需要Mock相关的接口
func TestCreateUser(t *testing.T) {
repo := new(MockUserRepository)
auth := new(MockAuthService)
notify := new(MockNotificationService)
logger := new(MockActivityLogger)
manager := NewUserManager(repo, auth, notify, logger)
user := User{ID: 1, Email: "[email protected]", Name: "Test"}
// 只设置需要的期望
repo.On("Create", user).Return(nil)
notify.On("SendEmail", user.Email, "Welcome", mock.Anything).Return(nil)
logger.On("LogActivity", user.ID, "user_created").Return(nil)
err := manager.CreateUser(user)
assert.NoError(t, err)
repo.AssertExpectations(t)
notify.AssertExpectations(t)
logger.AssertExpectations(t)
}
---
02.依赖注入模式
a.构造函数注入
通过构造函数传入依赖接口是最推荐的方式。依赖在对象创建时就确定,不可变,线程安全。测试时可以轻松注入Mock对象。构造函数应该只接收接口类型,不接收具体实现。
b.方法注入
某些情况下可以通过方法参数传入依赖。这适用于依赖只在特定操作中使用的场景。方法注入比构造函数注入更灵活,但也增加了方法签名的复杂度。
c.代码示例
---
// 依赖注入模式
package dependencyinjection
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// 依赖接口定义
type Clock interface {
Now() time.Time
}
type IDGenerator interface {
NextID() int64
}
type Repository interface {
Save(ctx context.Context, data interface{}) error
}
// 构造函数注入示例
type OrderService struct {
clock Clock
idGen IDGenerator
repo Repository
}
func NewOrderService(clock Clock, idGen IDGenerator, repo Repository) *OrderService {
return &OrderService{
clock: clock,
idGen: idGen,
repo: repo,
}
}
func (s *OrderService) CreateOrder(ctx context.Context, amount float64) error {
order := Order{
ID: s.idGen.NextID(),
Amount: amount,
CreatedAt: s.clock.Now(),
}
return s.repo.Save(ctx, order)
}
type Order struct {
ID int64
Amount float64
CreatedAt time.Time
}
// 方法注入示例
type ReportGenerator struct{}
func (g *ReportGenerator) Generate(ctx context.Context, fetcher DataFetcher) (*Report, error) {
data, err := fetcher.Fetch(ctx)
if err != nil {
return nil, err
}
return &Report{Data: data}, nil
}
type DataFetcher interface {
Fetch(ctx context.Context) ([]byte, error)
}
type Report struct {
Data []byte
}
// Mock实现
type MockClock struct {
mock.Mock
}
func (m *MockClock) Now() time.Time {
args := m.Called()
return args.Get(0).(time.Time)
}
type MockIDGenerator struct {
mock.Mock
}
func (m *MockIDGenerator) NextID() int64 {
args := m.Called()
return args.Get(0).(int64)
}
type MockRepository struct {
mock.Mock
}
func (m *MockRepository) Save(ctx context.Context, data interface{}) error {
args := m.Called(ctx, data)
return args.Error(0)
}
type MockDataFetcher struct {
mock.Mock
}
func (m *MockDataFetcher) Fetch(ctx context.Context) ([]byte, error) {
args := m.Called(ctx)
return args.Get(0).([]byte), args.Error(1)
}
// 测试构造函数注入
func TestConstructorInjection(t *testing.T) {
clock := new(MockClock)
idGen := new(MockIDGenerator)
repo := new(MockRepository)
service := NewOrderService(clock, idGen, repo)
ctx := context.Background()
fixedTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
// 设置期望
idGen.On("NextID").Return(int64(12345))
clock.On("Now").Return(fixedTime)
repo.On("Save", ctx, mock.MatchedBy(func(o interface{}) bool {
order, ok := o.(Order)
return ok && order.ID == 12345 && order.Amount == 99.99
})).Return(nil)
// 执行
err := service.CreateOrder(ctx, 99.99)
// 验证
assert.NoError(t, err)
clock.AssertExpectations(t)
idGen.AssertExpectations(t)
repo.AssertExpectations(t)
}
// 测试方法注入
func TestMethodInjection(t *testing.T) {
generator := &ReportGenerator{}
fetcher := new(MockDataFetcher)
ctx := context.Background()
expectedData := []byte("report data")
fetcher.On("Fetch", ctx).Return(expectedData, nil)
report, err := generator.Generate(ctx, fetcher)
assert.NoError(t, err)
assert.Equal(t, expectedData, report.Data)
fetcher.AssertExpectations(t)
}
// 工厂模式结合依赖注入
type ServiceFactory struct {
clock Clock
repo Repository
}
func NewServiceFactory(clock Clock, repo Repository) *ServiceFactory {
return &ServiceFactory{clock: clock, repo: repo}
}
func (f *ServiceFactory) CreateOrderService(idGen IDGenerator) *OrderService {
return NewOrderService(f.clock, idGen, f.repo)
}
func TestFactoryPattern(t *testing.T) {
clock := new(MockClock)
repo := new(MockRepository)
idGen := new(MockIDGenerator)
factory := NewServiceFactory(clock, repo)
service := factory.CreateOrderService(idGen)
ctx := context.Background()
fixedTime := time.Now()
clock.On("Now").Return(fixedTime)
idGen.On("NextID").Return(int64(999))
repo.On("Save", ctx, mock.Anything).Return(nil)
err := service.CreateOrder(ctx, 50.0)
assert.NoError(t, err)
}
---
03.接口适配器
a.适配器模式
当需要Mock第三方库或标准库时,可以创建适配器接口。适配器包装原始类型,提供可测试的接口。这遵循依赖倒置原则,使代码不直接依赖具体实现。
b.标准库封装
对于time、os、http等标准库,可以定义接口进行封装。这使得依赖时间、文件系统、网络的代码变得可测试。适配器接口应该简洁,只暴露需要的方法。
c.代码示例
---
// 接口适配器
package adapter
import (
"io"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// 时间适配器
type TimeProvider interface {
Now() time.Time
Sleep(d time.Duration)
}
type RealTimeProvider struct{}
func (r *RealTimeProvider) Now() time.Time {
return time.Now()
}
func (r *RealTimeProvider) Sleep(d time.Duration) {
time.Sleep(d)
}
// 文件系统适配器
type FileSystem interface {
ReadFile(path string) ([]byte, error)
WriteFile(path string, data []byte) error
Exists(path string) bool
}
type RealFileSystem struct{}
func (f *RealFileSystem) ReadFile(path string) ([]byte, error) {
return os.ReadFile(path)
}
func (f *RealFileSystem) WriteFile(path string, data []byte) error {
return os.WriteFile(path, data, 0644)
}
func (f *RealFileSystem) Exists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// HTTP客户端适配器
type HTTPClient interface {
Get(url string) ([]byte, error)
Post(url string, body io.Reader) ([]byte, error)
}
type RealHTTPClient struct {
// 可以包装 *http.Client
}
func (h *RealHTTPClient) Get(url string) ([]byte, error) {
// 实际HTTP请求
return nil, nil
}
func (h *RealHTTPClient) Post(url string, body io.Reader) ([]byte, error) {
// 实际HTTP请求
return nil, nil
}
// 使用适配器的业务逻辑
type DataSync struct {
time TimeProvider
fs FileSystem
http HTTPClient
}
func NewDataSync(time TimeProvider, fs FileSystem, http HTTPClient) *DataSync {
return &DataSync{time: time, fs: fs, http: http}
}
func (s *DataSync) SyncData(url string, localPath string) error {
// 检查本地文件是否存在
if s.fs.Exists(localPath) {
return nil
}
// 从远程获取数据
data, err := s.http.Get(url)
if err != nil {
return err
}
// 写入本地
return s.fs.WriteFile(localPath, data)
}
// Mock实现
type MockTimeProvider struct {
mock.Mock
}
func (m *MockTimeProvider) Now() time.Time {
args := m.Called()
return args.Get(0).(time.Time)
}
func (m *MockTimeProvider) Sleep(d time.Duration) {
m.Called(d)
}
type MockFileSystem struct {
mock.Mock
}
func (m *MockFileSystem) ReadFile(path string) ([]byte, error) {
args := m.Called(path)
return args.Get(0).([]byte), args.Error(1)
}
func (m *MockFileSystem) WriteFile(path string, data []byte) error {
args := m.Called(path, data)
return args.Error(0)
}
func (m *MockFileSystem) Exists(path string) bool {
args := m.Called(path)
return args.Bool(0)
}
type MockHTTPClient struct {
mock.Mock
}
func (m *MockHTTPClient) Get(url string) ([]byte, error) {
args := m.Called(url)
return args.Get(0).([]byte), args.Error(1)
}
func (m *MockHTTPClient) Post(url string, body io.Reader) ([]byte, error) {
args := m.Called(url, body)
return args.Get(0).([]byte), args.Error(1)
}
// 测试
func TestDataSync(t *testing.T) {
timeProvider := new(MockTimeProvider)
fs := new(MockFileSystem)
httpClient := new(MockHTTPClient)
sync := NewDataSync(timeProvider, fs, httpClient)
url := "https://api.example.com/data"
localPath := "/tmp/data.json"
expectedData := []byte(`{"key":"value"}`)
// 文件不存在,需要下载
fs.On("Exists", localPath).Return(false)
httpClient.On("Get", url).Return(expectedData, nil)
fs.On("WriteFile", localPath, expectedData).Return(nil)
err := sync.SyncData(url, localPath)
assert.NoError(t, err)
fs.AssertExpectations(t)
httpClient.AssertExpectations(t)
}
func TestDataSyncFileExists(t *testing.T) {
fs := new(MockFileSystem)
httpClient := new(MockHTTPClient)
sync := NewDataSync(&RealTimeProvider{}, fs, httpClient)
localPath := "/tmp/data.json"
// 文件已存在,不需要下载
fs.On("Exists", localPath).Return(true)
err := sync.SyncData("https://api.example.com/data", localPath)
assert.NoError(t, err)
fs.AssertExpectations(t)
// HTTP客户端不应该被调用
httpClient.AssertNotCalled(t, "Get", mock.Anything)
}
---
04.接口组合
a.组合多个接口
Go支持接口组合,可以将多个小接口组合成大接口。这提供了灵活性,可以根据需要使用小接口或组合接口。测试时通常使用小接口,生产代码可以使用组合接口。
b.条件依赖
某些依赖可能是可选的。可以使用接口组合或者nil检查来实现可选依赖。Mock可选依赖时,可以传入nil或者使用Maybe()期望。
c.代码示例
---
// 接口组合
package composition
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// 基础接口
type Reader interface {
Read(key string) (string, error)
}
type Writer interface {
Write(key, value string) error
}
type Deleter interface {
Delete(key string) error
}
// 组合接口
type ReadWriter interface {
Reader
Writer
}
type Storage interface {
Reader
Writer
Deleter
}
// 可选功能接口
type CacheInvalidator interface {
InvalidateCache(key string)
}
// 业务逻辑:只依赖需要的接口
type DataService struct {
storage ReadWriter // 必需依赖
invalidator CacheInvalidator // 可选依赖
}
func NewDataService(storage ReadWriter, invalidator CacheInvalidator) *DataService {
return &DataService{
storage: storage,
invalidator: invalidator,
}
}
func (s *DataService) UpdateData(key, value string) error {
// 写入数据
if err := s.storage.Write(key, value); err != nil {
return err
}
// 如果有缓存失效器,使用它
if s.invalidator != nil {
s.invalidator.InvalidateCache(key)
}
return nil
}
// Mock实现
type MockReader struct {
mock.Mock
}
func (m *MockReader) Read(key string) (string, error) {
args := m.Called(key)
return args.String(0), args.Error(1)
}
type MockWriter struct {
mock.Mock
}
func (m *MockWriter) Write(key, value string) error {
args := m.Called(key, value)
return args.Error(0)
}
type MockReadWriter struct {
MockReader
MockWriter
}
type MockStorage struct {
mock.Mock
}
func (m *MockStorage) Read(key string) (string, error) {
args := m.Called(key)
return args.String(0), args.Error(1)
}
func (m *MockStorage) Write(key, value string) error {
args := m.Called(key, value)
return args.Error(0)
}
func (m *MockStorage) Delete(key string) error {
args := m.Called(key)
return args.Error(0)
}
type MockCacheInvalidator struct {
mock.Mock
}
func (m *MockCacheInvalidator) InvalidateCache(key string) {
m.Called(key)
}
// 测试:使用组合Mock
func TestWithCacheInvalidator(t *testing.T) {
storage := new(MockStorage)
invalidator := new(MockCacheInvalidator)
service := NewDataService(storage, invalidator)
storage.On("Write", "key1", "value1").Return(nil)
invalidator.On("InvalidateCache", "key1").Return()
err := service.UpdateData("key1", "value1")
assert.NoError(t, err)
storage.AssertExpectations(t)
invalidator.AssertExpectations(t)
}
// 测试:不使用缓存失效器
func TestWithoutCacheInvalidator(t *testing.T) {
storage := new(MockStorage)
service := NewDataService(storage, nil)
storage.On("Write", "key1", "value1").Return(nil)
err := service.UpdateData("key1", "value1")
assert.NoError(t, err)
storage.AssertExpectations(t)
}
// 分离的Mock测试
func TestSeparateMocks(t *testing.T) {
reader := new(MockReader)
writer := new(MockWriter)
// 创建组合Mock
rw := &MockReadWriter{
MockReader: *reader,
MockWriter: *writer,
}
service := NewDataService(rw, nil)
writer.On("Write", "test", "data").Return(nil)
err := service.UpdateData("test", "data")
assert.NoError(t, err)
writer.AssertExpectations(t)
}
---
4.7 最佳实践
01.Mock对象命名规范
a.命名约定
Mock结构体应该以Mock为前缀,后接接口名。例如接口UserService的Mock命名为MockUserService。这种命名清晰表明类型的用途,便于识别和维护。避免使用FakeUserService、StubUserService等容易混淆的名称。
b.包组织
Mock定义可以放在测试文件中,使用_test后缀。对于被多个测试包使用的Mock,可以创建独立的mock或testing子包。不要将Mock放在生产代码包中,保持生产代码的纯净性。
c.代码示例
---
// Mock命名和组织最佳实践
package naming
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// ✅ 正确:清晰的Mock命名
type UserService interface {
GetUser(id int) (*User, error)
SaveUser(user *User) error
}
type MockUserService struct {
mock.Mock
}
func (m *MockUserService) GetUser(id int) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func (m *MockUserService) SaveUser(user *User) error {
args := m.Called(user)
return args.Error(0)
}
// ❌ 错误:命名不清晰
type FakeUserService struct {
mock.Mock
}
type StubUserService struct {
mock.Mock
}
type TestUserService struct {
mock.Mock
}
type User struct {
ID int
Name string
}
// 组织方式1:Mock定义在测试文件中
func TestUserServiceUsage(t *testing.T) {
mockService := new(MockUserService)
expectedUser := &User{ID: 1, Name: "Alice"}
mockService.On("GetUser", 1).Return(expectedUser, nil)
user, err := mockService.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)
mockService.AssertExpectations(t)
}
// 组织方式2:创建测试辅助函数
func setupUserServiceMock(t *testing.T) *MockUserService {
m := new(MockUserService)
return m
}
func TestWithHelper(t *testing.T) {
mockService := setupUserServiceMock(t)
user := &User{ID: 2, Name: "Bob"}
mockService.On("SaveUser", user).Return(nil)
err := mockService.SaveUser(user)
assert.NoError(t, err)
mockService.AssertExpectations(t)
}
// 组织方式3:表格驱动测试中的Mock
func TestTableDriven(t *testing.T) {
tests := []struct {
name string
userID int
setupMock func(*MockUserService)
expectError bool
}{
{
name: "user found",
userID: 1,
setupMock: func(m *MockUserService) {
m.On("GetUser", 1).Return(&User{ID: 1, Name: "Alice"}, nil)
},
expectError: false,
},
{
name: "user not found",
userID: 999,
setupMock: func(m *MockUserService) {
m.On("GetUser", 999).Return(nil, assert.AnError)
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockService := new(MockUserService)
tt.setupMock(mockService)
_, err := mockService.GetUser(tt.userID)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
mockService.AssertExpectations(t)
})
}
}
---
02.测试隔离性
a.每个测试独立Mock
每个测试应该创建自己的Mock实例,不要在测试间共享Mock。共享Mock会导致状态污染,一个测试的期望影响另一个测试。子测试也应该创建独立的Mock。
b.清理和重置
testify的Mock不需要显式清理,每个测试创建新实例即可。避免使用全局Mock变量。如果必须使用同一个Mock运行多次,可以使用ExpectedCalls字段清空期望,但不推荐这种做法。
c.代码示例
---
// 测试隔离性最佳实践
package isolation
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type Calculator interface {
Add(a, b int) int
Multiply(a, b int) int
}
type MockCalculator struct {
mock.Mock
}
func (m *MockCalculator) Add(a, b int) int {
args := m.Called(a, b)
return args.Int(0)
}
func (m *MockCalculator) Multiply(a, b int) int {
args := m.Called(a, b)
return args.Int(0)
}
// ✅ 正确:每个测试独立Mock
func TestAddition(t *testing.T) {
calc := new(MockCalculator)
calc.On("Add", 2, 3).Return(5)
result := calc.Add(2, 3)
assert.Equal(t, 5, result)
calc.AssertExpectations(t)
}
func TestMultiplication(t *testing.T) {
calc := new(MockCalculator)
calc.On("Multiply", 2, 3).Return(6)
result := calc.Multiply(2, 3)
assert.Equal(t, 6, result)
calc.AssertExpectations(t)
}
// ✅ 正确:子测试独立Mock
func TestCalculatorOperations(t *testing.T) {
t.Run("addition", func(t *testing.T) {
calc := new(MockCalculator)
calc.On("Add", 1, 1).Return(2)
result := calc.Add(1, 1)
assert.Equal(t, 2, result)
calc.AssertExpectations(t)
})
t.Run("multiplication", func(t *testing.T) {
calc := new(MockCalculator)
calc.On("Multiply", 2, 2).Return(4)
result := calc.Multiply(2, 2)
assert.Equal(t, 4, result)
calc.AssertExpectations(t)
})
}
// ❌ 错误:全局共享Mock(不推荐)
var globalCalc *MockCalculator
func TestWithGlobalMock1(t *testing.T) {
// 问题:全局Mock被多个测试共享
globalCalc = new(MockCalculator)
globalCalc.On("Add", 1, 1).Return(2)
result := globalCalc.Add(1, 1)
assert.Equal(t, 2, result)
}
func TestWithGlobalMock2(t *testing.T) {
// 问题:可能受到前一个测试的影响
result := globalCalc.Add(1, 1)
assert.Equal(t, 2, result)
}
// ✅ 正确:使用setup函数创建独立Mock
func setupCalculator() *MockCalculator {
return new(MockCalculator)
}
func TestWithSetup1(t *testing.T) {
calc := setupCalculator()
calc.On("Add", 5, 5).Return(10)
result := calc.Add(5, 5)
assert.Equal(t, 10, result)
calc.AssertExpectations(t)
}
func TestWithSetup2(t *testing.T) {
calc := setupCalculator()
calc.On("Multiply", 3, 3).Return(9)
result := calc.Multiply(3, 3)
assert.Equal(t, 9, result)
calc.AssertExpectations(t)
}
// ✅ 正确:表格驱动测试,每个case独立Mock
func TestTableDrivenWithIsolation(t *testing.T) {
tests := []struct {
name string
op string
a, b int
expected int
}{
{"add positive", "add", 2, 3, 5},
{"add negative", "add", -1, -2, -3},
{"multiply positive", "multiply", 2, 3, 6},
{"multiply zero", "multiply", 2, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 每个case创建新Mock
calc := new(MockCalculator)
if tt.op == "add" {
calc.On("Add", tt.a, tt.b).Return(tt.expected)
result := calc.Add(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
} else {
calc.On("Multiply", tt.a, tt.b).Return(tt.expected)
result := calc.Multiply(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
}
calc.AssertExpectations(t)
})
}
}
---
03.合理使用Mock
a.什么时候使用Mock
当依赖慢速外部资源如数据库、网络、文件系统时使用Mock。当依赖难以控制如随机数、时间时使用Mock。当需要模拟错误场景时使用Mock。简单的纯函数不需要Mock。
b.什么时候不用Mock
单元测试值对象、工具函数等无依赖的代码时不需要Mock。集成测试验证真实交互时不用Mock。过度Mock会使测试脆弱,失去对真实行为的验证。
c.代码示例
---
// 合理使用Mock的场景
package appropriate
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// ✅ 场景1:需要Mock - 依赖外部服务
type PaymentGateway interface {
Charge(amount float64, token string) error
}
type MockPaymentGateway struct {
mock.Mock
}
func (m *MockPaymentGateway) Charge(amount float64, token string) error {
args := m.Called(amount, token)
return args.Error(0)
}
type OrderProcessor struct {
gateway PaymentGateway
}
func (p *OrderProcessor) ProcessOrder(amount float64, token string) error {
return p.gateway.Charge(amount, token)
}
func TestProcessOrderWithMock(t *testing.T) {
gateway := new(MockPaymentGateway)
processor := &OrderProcessor{gateway: gateway}
// Mock支付网关,避免真实支付
gateway.On("Charge", 99.99, "token123").Return(nil)
err := processor.ProcessOrder(99.99, "token123")
assert.NoError(t, err)
gateway.AssertExpectations(t)
}
// ✅ 场景2:需要Mock - 模拟错误场景
func TestProcessOrderFailure(t *testing.T) {
gateway := new(MockPaymentGateway)
processor := &OrderProcessor{gateway: gateway}
// Mock支付失败
gateway.On("Charge", 99.99, "invalid_token").
Return(errors.New("payment declined"))
err := processor.ProcessOrder(99.99, "invalid_token")
assert.Error(t, err)
gateway.AssertExpectations(t)
}
// ✅ 场景3:需要Mock - 控制时间
type TimeProvider interface {
Now() time.Time
}
type MockTimeProvider struct {
mock.Mock
}
func (m *MockTimeProvider) Now() time.Time {
args := m.Called()
return args.Get(0).(time.Time)
}
type Scheduler struct {
time TimeProvider
}
func (s *Scheduler) IsExpired(expiryTime time.Time) bool {
return s.time.Now().After(expiryTime)
}
func TestScheduler(t *testing.T) {
timeMock := new(MockTimeProvider)
scheduler := &Scheduler{time: timeMock}
// 固定时间,便于测试
fixedTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
timeMock.On("Now").Return(fixedTime)
expiryTime := time.Date(2024, 1, 1, 11, 0, 0, 0, time.UTC)
assert.True(t, scheduler.IsExpired(expiryTime))
timeMock.AssertExpectations(t)
}
// ❌ 场景4:不需要Mock - 纯函数
func Add(a, b int) int {
return a + b
}
func TestAddNoMock(t *testing.T) {
// 直接测试,不需要Mock
result := Add(2, 3)
assert.Equal(t, 5, result)
}
// ❌ 场景5:不需要Mock - 简单值对象
type Point struct {
X, Y int
}
func (p Point) Distance(other Point) float64 {
dx := float64(p.X - other.X)
dy := float64(p.Y - other.Y)
return dx*dx + dy*dy
}
func TestPointDistance(t *testing.T) {
// 值对象无依赖,直接测试
p1 := Point{X: 0, Y: 0}
p2 := Point{X: 3, Y: 4}
distance := p1.Distance(p2)
assert.Equal(t, 25.0, distance)
}
// ⚠️ 场景6:谨慎使用Mock - 不要过度Mock
type UserRepository interface {
GetUser(id int) (*User, error)
}
type UserValidator interface {
Validate(user *User) error
}
type UserFormatter interface {
Format(user *User) string
}
type User struct {
ID int
Name string
}
// 如果Formatter只是简单的字符串格式化,不需要Mock
type SimpleFormatter struct{}
func (f *SimpleFormatter) Format(user *User) string {
return user.Name
}
func TestWithoutOverMocking(t *testing.T) {
// Repository需要Mock(依赖数据库)
repo := &struct {
mock.Mock
UserRepository
}{}
// Formatter不需要Mock(纯函数)
formatter := &SimpleFormatter{}
user := &User{ID: 1, Name: "Alice"}
formatted := formatter.Format(user)
assert.Equal(t, "Alice", formatted)
}
---
04.Mock维护策略
a.保持Mock简单
Mock应该简单直接,反映接口的真实行为。避免在Mock中实现复杂业务逻辑。如果Mock变得复杂,考虑是否接口设计有问题。过于复杂的Mock可能表明接口职责过多。
b.文档和注释
为Mock添加必要的注释,说明模拟的场景和行为。复杂的期望设置应该有注释解释原因。这帮助其他开发者理解测试意图,便于维护。
c.代码示例
---
// Mock维护最佳实践
package maintenance
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type EmailService interface {
Send(to, subject, body string) error
SendBatch(recipients []string, subject, body string) error
ValidateEmail(email string) bool
}
type MockEmailService struct {
mock.Mock
}
func (m *MockEmailService) Send(to, subject, body string) error {
args := m.Called(to, subject, body)
return args.Error(0)
}
func (m *MockEmailService) SendBatch(recipients []string, subject, body string) error {
args := m.Called(recipients, subject, body)
return args.Error(0)
}
func (m *MockEmailService) ValidateEmail(email string) bool {
args := m.Called(email)
return args.Bool(0)
}
// ✅ 良好实践:清晰的测试结构
func TestSendWelcomeEmail(t *testing.T) {
// Arrange - 准备测试数据和Mock
emailService := new(MockEmailService)
userEmail := "[email protected]"
// Mock设置:期望发送欢迎邮件
emailService.On("Send",
userEmail,
"Welcome to Our Service",
mock.MatchedBy(func(body string) bool {
// 验证邮件正文包含必要信息
return len(body) > 0
}),
).Return(nil)
// Act - 执行测试
err := emailService.Send(userEmail, "Welcome to Our Service", "Welcome!")
// Assert - 验证结果
assert.NoError(t, err)
emailService.AssertExpectations(t)
}
// ✅ 良好实践:使用辅助函数简化Mock设置
func setupEmailServiceMock() *MockEmailService {
m := new(MockEmailService)
// 默认行为:邮件验证
m.On("ValidateEmail", mock.MatchedBy(func(email string) bool {
return len(email) > 0 && len(email) < 100
})).Return(true)
return m
}
func TestWithHelper(t *testing.T) {
emailService := setupEmailServiceMock()
// 只需设置特定测试的期望
emailService.On("Send", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
err := emailService.Send("[email protected]", "Test", "Body")
assert.NoError(t, err)
}
// ✅ 良好实践:测试常见错误场景
func TestEmailServiceErrors(t *testing.T) {
tests := []struct {
name string
setupMock func(*MockEmailService)
email string
expectedError string
}{
{
name: "invalid email format",
setupMock: func(m *MockEmailService) {
m.On("Send", "invalid-email", mock.Anything, mock.Anything).
Return(errors.New("invalid email format"))
},
email: "invalid-email",
expectedError: "invalid email format",
},
{
name: "smtp server error",
setupMock: func(m *MockEmailService) {
m.On("Send", "[email protected]", mock.Anything, mock.Anything).
Return(errors.New("SMTP connection failed"))
},
email: "[email protected]",
expectedError: "SMTP connection failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
emailService := new(MockEmailService)
tt.setupMock(emailService)
err := emailService.Send(tt.email, "Subject", "Body")
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError)
emailService.AssertExpectations(t)
})
}
}
// ✅ 良好实践:注释复杂的Mock行为
func TestComplexScenario(t *testing.T) {
emailService := new(MockEmailService)
recipients := []string{"[email protected]", "[email protected]", "[email protected]"}
// 复杂场景:批量发送,模拟部分成功
// 第一次调用失败(模拟临时网络问题)
emailService.On("SendBatch", recipients, "Newsletter", mock.Anything).
Return(errors.New("temporary network error")).
Once()
// 第二次调用成功(模拟重试成功)
emailService.On("SendBatch", recipients, "Newsletter", mock.Anything).
Return(nil).
Once()
// 第一次尝试
err1 := emailService.SendBatch(recipients, "Newsletter", "Content")
assert.Error(t, err1, "第一次应该失败")
// 重试
err2 := emailService.SendBatch(recipients, "Newsletter", "Content")
assert.NoError(t, err2, "第二次应该成功")
emailService.AssertExpectations(t)
}
// ✅ 良好实践:提取共用的Mock配置
type testConfig struct {
emailService *MockEmailService
}
func setupTestConfig() *testConfig {
emailService := new(MockEmailService)
// 设置通用的Mock行为
emailService.On("ValidateEmail", mock.Anything).
Return(func(email string) bool {
// 简单的邮件验证逻辑
return len(email) > 3
})
return &testConfig{
emailService: emailService,
}
}
func TestWithConfig(t *testing.T) {
config := setupTestConfig()
// 添加测试特定的期望
config.emailService.On("Send", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
err := config.emailService.Send("[email protected]", "Test", "Body")
assert.NoError(t, err)
config.emailService.AssertExpectations(t)
}
---
5 测试套件详解
5.1 Suite基础
01.Suite概念
a.什么是Suite
testify的suite包提供了测试套件功能,类似于其他语言测试框架的Test Class。Suite允许将相关的测试组织在一起,共享设置和清理逻辑。通过嵌入suite.Suite类型,结构体获得了丰富的测试功能和生命周期管理能力。
b.Suite的优势
Suite提供了统一的测试前后处理机制,避免重复代码。支持SetupTest、TearDownTest等钩子函数,在测试生命周期的不同阶段执行操作。Suite使测试代码更有组织性,特别适合需要复杂初始化的测试场景。
c.代码示例
---
// Suite基础概念
package suitebasic
import (
"testing"
"github.com/stretchr/testify/suite"
)
// 定义测试套件
type BasicSuite struct {
suite.Suite
// 可以添加共享字段
value int
}
// 测试方法:必须以Test开头
func (s *BasicSuite) TestSimple() {
s.Equal(1, 1)
s.True(true)
s.NotNil(s)
}
func (s *BasicSuite) TestAssertions() {
s.Equal(5, 2+3)
s.Contains("hello world", "world")
s.Greater(10, 5)
}
func (s *BasicSuite) TestWithSharedState() {
// 访问套件字段
s.value = 42
s.Equal(42, s.value)
}
// 运行套件:必需的入口函数
func TestBasicSuite(t *testing.T) {
suite.Run(t, new(BasicSuite))
}
// 更复杂的套件示例
type UserServiceSuite struct {
suite.Suite
service *UserService
users []*User
}
type UserService struct {
users map[int]*User
}
func NewUserService() *UserService {
return &UserService{
users: make(map[int]*User),
}
}
func (s *UserService) AddUser(user *User) {
s.users[user.ID] = user
}
func (s *UserService) GetUser(id int) *User {
return s.users[id]
}
func (s *UserService) DeleteUser(id int) {
delete(s.users, id)
}
type User struct {
ID int
Name string
}
func (s *UserServiceSuite) TestAddUser() {
user := &User{ID: 1, Name: "Alice"}
s.service.AddUser(user)
retrieved := s.service.GetUser(1)
s.NotNil(retrieved)
s.Equal("Alice", retrieved.Name)
}
func (s *UserServiceSuite) TestGetUser() {
user := &User{ID: 2, Name: "Bob"}
s.service.AddUser(user)
result := s.service.GetUser(2)
s.Equal(user, result)
}
func (s *UserServiceSuite) TestDeleteUser() {
user := &User{ID: 3, Name: "Charlie"}
s.service.AddUser(user)
s.service.DeleteUser(3)
result := s.service.GetUser(3)
s.Nil(result)
}
func TestUserServiceSuite(t *testing.T) {
suite.Run(t, new(UserServiceSuite))
}
---
02.Suite结构定义
a.嵌入suite.Suite
创建Suite的第一步是定义一个结构体并嵌入suite.Suite。这个嵌入为结构体提供了所有断言方法和Suite功能。结构体可以添加自己的字段存储测试所需的状态、依赖、配置等。
b.添加测试字段
在Suite结构体中可以定义字段存储测试依赖如数据库连接、Mock对象、测试数据等。这些字段在SetupSuite或SetupTest中初始化,在测试方法中使用,在TearDown方法中清理。字段让测试间共享资源变得简单。
c.代码示例
---
// Suite结构定义
package suitestructure
import (
"database/sql"
"testing"
"github.com/stretchr/testify/suite"
)
// 基础Suite:只有suite.Suite
type MinimalSuite struct {
suite.Suite
}
func (s *MinimalSuite) TestExample() {
s.True(true)
}
func TestMinimalSuite(t *testing.T) {
suite.Run(t, new(MinimalSuite))
}
// 带共享资源的Suite
type DatabaseSuite struct {
suite.Suite
db *sql.DB
config Config
}
type Config struct {
Host string
Port int
}
// 带Mock的Suite
type ServiceSuite struct {
suite.Suite
mockDB *MockDatabase
mockCache *MockCache
service *Service
}
type MockDatabase struct {
data map[string]string
}
func (m *MockDatabase) Get(key string) string {
return m.data[key]
}
func (m *MockDatabase) Set(key, value string) {
m.data[key] = value
}
type MockCache struct {
items map[string]interface{}
}
func (m *MockCache) Get(key string) interface{} {
return m.items[key]
}
func (m *MockCache) Set(key string, value interface{}) {
m.items[key] = value
}
type Service struct {
db *MockDatabase
cache *MockCache
}
func NewService(db *MockDatabase, cache *MockCache) *Service {
return &Service{db: db, cache: cache}
}
func (s *Service) GetValue(key string) string {
// 先查缓存
if val := s.cache.Get(key); val != nil {
return val.(string)
}
// 查数据库
val := s.db.Get(key)
// 写入缓存
s.cache.Set(key, val)
return val
}
func (s *ServiceSuite) TestGetValueFromCache() {
// 测试会使用Suite的字段
s.mockCache.Set("key1", "cached_value")
result := s.service.GetValue("key1")
s.Equal("cached_value", result)
}
func (s *ServiceSuite) TestGetValueFromDatabase() {
s.mockDB.Set("key2", "db_value")
result := s.service.GetValue("key2")
s.Equal("db_value", result)
// 验证已缓存
s.Equal("db_value", s.mockCache.Get("key2"))
}
func TestServiceSuite(t *testing.T) {
suite.Run(t, new(ServiceSuite))
}
// 带测试数据的Suite
type DataProcessingSuite struct {
suite.Suite
testData []TestData
processor *DataProcessor
expectedResults map[string]interface{}
}
type TestData struct {
Input string
Expected string
}
type DataProcessor struct{}
func (p *DataProcessor) Process(input string) string {
return input
}
func (s *DataProcessingSuite) TestProcessData() {
for _, data := range s.testData {
result := s.processor.Process(data.Input)
s.Equal(data.Expected, result)
}
}
func TestDataProcessingSuite(t *testing.T) {
suite.Run(t, new(DataProcessingSuite))
}
// 嵌套组合的Suite
type BaseSuite struct {
suite.Suite
commonConfig Config
}
type ExtendedSuite struct {
BaseSuite
additionalData string
}
func (s *ExtendedSuite) TestUsingBaseFields() {
s.NotNil(s.commonConfig)
s.NotEmpty(s.additionalData)
}
func TestExtendedSuite(t *testing.T) {
suite.Run(t, new(ExtendedSuite))
}
---
03.运行Suite
a.suite.Run方法
suite.Run是运行测试套件的入口。它接收*testing.T和Suite实例,自动发现所有Test开头的方法并执行。Run会正确调用所有生命周期钩子。每个Suite需要一个TestXxxSuite函数作为go test的入口点。
b.测试方法命名
Suite中的测试方法必须以Test开头才会被执行。方法签名是func (s *SuiteType) TestXxx(),没有参数,没有返回值。可以有任意数量的测试方法。非Test开头的方法不会被执行,可以用作辅助函数。
c.代码示例
---
// 运行Suite详解
package suiterun
import (
"testing"
"github.com/stretchr/testify/suite"
)
type ExampleSuite struct {
suite.Suite
executionOrder []string
}
// ✅ 会被执行:以Test开头
func (s *ExampleSuite) TestFirst() {
s.executionOrder = append(s.executionOrder, "TestFirst")
s.True(true)
}
func (s *ExampleSuite) TestSecond() {
s.executionOrder = append(s.executionOrder, "TestSecond")
s.Equal(1, 1)
}
func (s *ExampleSuite) TestThird() {
s.executionOrder = append(s.executionOrder, "TestThird")
s.NotNil(s)
}
// ❌ 不会被执行:不以Test开头
func (s *ExampleSuite) helperMethod() string {
return "helper"
}
func (s *ExampleSuite) validateData(data string) bool {
return len(data) > 0
}
// ✅ 测试可以调用辅助方法
func (s *ExampleSuite) TestWithHelper() {
result := s.helperMethod()
s.Equal("helper", result)
s.True(s.validateData("test"))
}
// 运行Suite的入口函数
func TestExampleSuite(t *testing.T) {
suite.Run(t, new(ExampleSuite))
}
// 多个Suite可以有各自的入口
type AnotherSuite struct {
suite.Suite
}
func (s *AnotherSuite) TestSomething() {
s.True(true)
}
func TestAnotherSuite(t *testing.T) {
suite.Run(t, new(AnotherSuite))
}
// 可以有多个测试入口运行同一个Suite(不常见)
func TestExampleSuiteAlternate(t *testing.T) {
s := new(ExampleSuite)
suite.Run(t, s)
}
// Suite实例化时可以预设字段
func TestExampleSuiteWithPreset(t *testing.T) {
s := &ExampleSuite{
executionOrder: []string{"preset"},
}
suite.Run(t, s)
}
// 测试方法命名示例
type NamingExampleSuite struct {
suite.Suite
}
// ✅ 正确的命名
func (s *NamingExampleSuite) TestUserCreation() {}
func (s *NamingExampleSuite) TestUserDeletion() {}
func (s *NamingExampleSuite) TestDataValidation() {}
func (s *NamingExampleSuite) Test_WithUnderscore() {}
func (s *NamingExampleSuite) TestMultipleWords() {}
// ❌ 这些不会被执行
func (s *NamingExampleSuite) testLowerCase() {} // 小写test
func (s *NamingExampleSuite) UserTest() {} // Test不在开头
func (s *NamingExampleSuite) DoSomething() {} // 没有Test
func (s *NamingExampleSuite) setupHelper() {} // 辅助方法
func TestNamingExampleSuite(t *testing.T) {
suite.Run(t, new(NamingExampleSuite))
}
// 验证测试执行
type ExecutionVerificationSuite struct {
suite.Suite
testCount int
}
func (s *ExecutionVerificationSuite) TestOne() {
s.testCount++
}
func (s *ExecutionVerificationSuite) TestTwo() {
s.testCount++
}
func (s *ExecutionVerificationSuite) TestThree() {
s.testCount++
}
func TestExecutionVerification(t *testing.T) {
s := &ExecutionVerificationSuite{}
suite.Run(t, s)
// 注意:testCount在每个测试方法间不会累积
// 因为每个测试方法执行时可能有独立的设置
}
---
5.2 生命周期钩子
01.SetupSuite和TearDownSuite
a.Suite级别的设置
SetupSuite在整个Suite开始前执行一次,用于初始化Suite级别的资源如数据库连接、配置加载等。TearDownSuite在所有测试完成后执行一次,用于清理资源。这两个方法适合开销大且可以在测试间共享的资源。
b.执行时机
SetupSuite是第一个执行的方法,在任何测试方法之前。TearDownSuite是最后执行的方法,无论测试成功还是失败都会执行。如果Setup Suite失败,后续的测试方法不会执行但TearDownSuite仍会执行。
c.代码示例
---
// SetupSuite和TearDownSuite
package suitelifecycle
import (
"fmt"
"testing"
"github.com/stretchr/testify/suite"
)
type LifecycleSuite struct {
suite.Suite
sharedResource string
executionLog []string
}
// Suite级别设置:整个Suite执行一次
func (s *LifecycleSuite) SetupSuite() {
s.executionLog = append(s.executionLog, "SetupSuite")
s.sharedResource = "initialized"
fmt.Println("SetupSuite: 初始化共享资源")
}
// Suite级别清理:整个Suite结束后执行一次
func (s *LifecycleSuite) TearDownSuite() {
s.executionLog = append(s.executionLog, "TearDownSuite")
s.sharedResource = ""
fmt.Println("TearDownSuite: 清理共享资源")
}
func (s *LifecycleSuite) TestOne() {
s.executionLog = append(s.executionLog, "TestOne")
s.Equal("initialized", s.sharedResource)
}
func (s *LifecycleSuite) TestTwo() {
s.executionLog = append(s.executionLog, "TestTwo")
s.NotEmpty(s.sharedResource)
}
func TestLifecycleSuite(t *testing.T) {
suite.Run(t, new(LifecycleSuite))
}
// 实际应用:数据库连接
type DatabaseSuite struct {
suite.Suite
dsn string
// db *sql.DB // 实际项目中
}
func (s *DatabaseSuite) SetupSuite() {
// 建立数据库连接(昂贵操作,只做一次)
s.dsn = "user:pass@tcp(localhost:3306)/testdb"
// s.db, _ = sql.Open("mysql", s.dsn)
fmt.Println("数据库连接已建立")
}
func (s *DatabaseSuite) TearDownSuite() {
// 关闭数据库连接
// if s.db != nil {
// s.db.Close()
// }
fmt.Println("数据库连接已关闭")
}
func (s *DatabaseSuite) TestQuery() {
s.NotEmpty(s.dsn)
// 使用s.db执行查询
}
func (s *DatabaseSuite) TestInsert() {
s.NotEmpty(s.dsn)
// 使用s.db执行插入
}
func TestDatabaseSuite(t *testing.T) {
suite.Run(t, new(DatabaseSuite))
}
// 实际应用:配置加载
type ConfigSuite struct {
suite.Suite
config map[string]string
}
func (s *ConfigSuite) SetupSuite() {
// 加载配置文件
s.config = map[string]string{
"api_url": "https://api.example.com",
"api_key": "secret_key",
"timeout": "30",
"max_retry": "3",
}
fmt.Println("配置已加载")
}
func (s *ConfigSuite) TearDownSuite() {
// 清理配置
s.config = nil
fmt.Println("配置已清理")
}
func (s *ConfigSuite) TestAPIConfiguration() {
s.Equal("https://api.example.com", s.config["api_url"])
s.NotEmpty(s.config["api_key"])
}
func (s *ConfigSuite) TestTimeoutConfiguration() {
s.Equal("30", s.config["timeout"])
}
func TestConfigSuite(t *testing.T) {
suite.Run(t, new(ConfigSuite))
}
---
02.SetupTest和TearDownTest
a.测试级别的设置
SetupTest在每个测试方法执行前都会调用,TearDownTest在每个测试方法后都会调用。这确保每个测试都有干净的初始状态。适合初始化测试特定的数据、重置Mock、清空缓存等操作。
b.隔离性保证
通过SetupTest,每个测试方法都获得独立的初始状态,避免测试间相互影响。即使一个测试修改了共享数据,下一个测试也会重新初始化。TearDownTest确保测试产生的副作用被清理。
c.代码示例
---
// SetupTest和TearDownTest
package testlifecycle
import (
"fmt"
"testing"
"github.com/stretchr/testify/suite"
)
type TestLevelSuite struct {
suite.Suite
counter int
testData []string
executionLog []string
}
// 每个测试前执行
func (s *TestLevelSuite) SetupTest() {
s.executionLog = append(s.executionLog, "SetupTest")
s.counter = 0
s.testData = []string{"initial"}
fmt.Printf("SetupTest: counter=%d, data=%v\n", s.counter, s.testData)
}
// 每个测试后执行
func (s *TestLevelSuite) TearDownTest() {
s.executionLog = append(s.executionLog, "TearDownTest")
fmt.Printf("TearDownTest: counter=%d, data=%v\n", s.counter, s.testData)
// 清理测试数据
s.testData = nil
}
func (s *TestLevelSuite) TestFirst() {
s.executionLog = append(s.executionLog, "TestFirst")
s.Equal(0, s.counter)
s.Len(s.testData, 1)
// 修改状态
s.counter = 10
s.testData = append(s.testData, "modified")
}
func (s *TestLevelSuite) TestSecond() {
s.executionLog = append(s.executionLog, "TestSecond")
// 状态已被SetupTest重置
s.Equal(0, s.counter)
s.Len(s.testData, 1)
s.Equal("initial", s.testData[0])
s.counter = 20
}
func (s *TestLevelSuite) TestThird() {
s.executionLog = append(s.executionLog, "TestThird")
// 再次重置
s.Equal(0, s.counter)
s.Len(s.testData, 1)
}
func TestTestLevelSuite(t *testing.T) {
suite.Run(t, new(TestLevelSuite))
}
// 实际应用:Mock重置
type MockTestSuite struct {
suite.Suite
mockDB *MockDB
mockCache *MockCache
}
type MockDB struct {
data map[string]string
}
type MockCache struct {
items map[string]interface{}
}
func (s *MockTestSuite) SetupTest() {
// 每个测试都创建新的Mock实例
s.mockDB = &MockDB{
data: make(map[string]string),
}
s.mockCache = &MockCache{
items: make(map[string]interface{}),
}
fmt.Println("SetupTest: Mock已重置")
}
func (s *MockTestSuite) TearDownTest() {
// 清理Mock
s.mockDB = nil
s.mockCache = nil
fmt.Println("TearDownTest: Mock已清理")
}
func (s *MockTestSuite) TestDBOperation() {
// 使用干净的mockDB
s.mockDB.data["key1"] = "value1"
s.Equal("value1", s.mockDB.data["key1"])
}
func (s *MockTestSuite) TestCacheOperation() {
// 使用干净的mockCache
s.mockCache.items["key2"] = "value2"
s.Equal("value2", s.mockCache.items["key2"])
// mockDB应该是空的
s.Empty(s.mockDB.data)
}
func TestMockTestSuite(t *testing.T) {
suite.Run(t, new(MockTestSuite))
}
// 实际应用:临时文件管理
type FileTestSuite struct {
suite.Suite
tempDir string
files []string
}
func (s *FileTestSuite) SetupTest() {
// 创建临时目录
s.tempDir = "/tmp/test_" + fmt.Sprint(s.T().Name())
s.files = []string{}
fmt.Printf("SetupTest: 临时目录创建 %s\n", s.tempDir)
}
func (s *FileTestSuite) TearDownTest() {
// 清理临时文件
for _, file := range s.files {
fmt.Printf("TearDownTest: 删除文件 %s\n", file)
}
s.files = nil
fmt.Printf("TearDownTest: 删除目录 %s\n", s.tempDir)
}
func (s *FileTestSuite) TestFileCreation() {
fileName := s.tempDir + "/test.txt"
s.files = append(s.files, fileName)
s.NotEmpty(fileName)
}
func (s *FileTestSuite) TestFileReading() {
// 每个测试都有独立的文件列表
s.Empty(s.files)
fileName := s.tempDir + "/data.txt"
s.files = append(s.files, fileName)
}
func TestFileTestSuite(t *testing.T) {
suite.Run(t, new(FileTestSuite))
}
---
03.BeforeTest和AfterTest
a.测试标识钩子
BeforeTest和AfterTest接收suite名称和测试方法名作为参数,可以根据不同的测试执行不同的逻辑。它们在SetupTest之后、测试方法之前(BeforeTest)和测试方法之后、TearDownTest之前(AfterTest)执行。
b.使用场景
这两个钩子适合需要根据测试名称进行特殊处理的场景,如针对特定测试设置不同的配置、记录测试执行时间、设置测试特定的Mock行为等。相比SetupTest更灵活但使用频率较低。
c.代码示例
---
// BeforeTest和AfterTest
package beforeafter
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type BeforeAfterSuite struct {
suite.Suite
startTime time.Time
testConfig map[string]string
executionLog []string
}
func (s *BeforeAfterSuite) SetupTest() {
s.executionLog = append(s.executionLog, "SetupTest")
s.testConfig = make(map[string]string)
}
// BeforeTest在SetupTest之后,测试方法之前执行
func (s *BeforeAfterSuite) BeforeTest(suiteName, testName string) {
s.executionLog = append(s.executionLog,
fmt.Sprintf("BeforeTest(%s, %s)", suiteName, testName))
// 记录开始时间
s.startTime = time.Now()
// 根据测试名称设置不同配置
switch testName {
case "TestFast":
s.testConfig["timeout"] = "1s"
case "TestSlow":
s.testConfig["timeout"] = "10s"
default:
s.testConfig["timeout"] = "5s"
}
fmt.Printf("BeforeTest: %s.%s 开始\n", suiteName, testName)
}
// AfterTest在测试方法之后,TearDownTest之前执行
func (s *BeforeAfterSuite) AfterTest(suiteName, testName string) {
s.executionLog = append(s.executionLog,
fmt.Sprintf("AfterTest(%s, %s)", suiteName, testName))
// 计算执行时间
duration := time.Since(s.startTime)
fmt.Printf("AfterTest: %s.%s 完成,耗时 %v\n",
suiteName, testName, duration)
}
func (s *BeforeAfterSuite) TearDownTest() {
s.executionLog = append(s.executionLog, "TearDownTest")
s.testConfig = nil
}
func (s *BeforeAfterSuite) TestFast() {
s.executionLog = append(s.executionLog, "TestFast")
s.Equal("1s", s.testConfig["timeout"])
time.Sleep(10 * time.Millisecond)
}
func (s *BeforeAfterSuite) TestSlow() {
s.executionLog = append(s.executionLog, "TestSlow")
s.Equal("10s", s.testConfig["timeout"])
time.Sleep(50 * time.Millisecond)
}
func (s *BeforeAfterSuite) TestDefault() {
s.executionLog = append(s.executionLog, "TestDefault")
s.Equal("5s", s.testConfig["timeout"])
}
func TestBeforeAfterSuite(t *testing.T) {
suite.Run(t, new(BeforeAfterSuite))
}
// 实际应用:测试分类
type CategorySuite struct {
suite.Suite
category string
metrics map[string]int
}
func (s *CategorySuite) SetupSuite() {
s.metrics = make(map[string]int)
}
func (s *CategorySuite) BeforeTest(suiteName, testName string) {
// 根据测试名称确定类别
if len(testName) > 8 && testName[:8] == "TestUnit" {
s.category = "unit"
} else if len(testName) > 15 && testName[:15] == "TestIntegration" {
s.category = "integration"
} else {
s.category = "other"
}
s.metrics[s.category]++
fmt.Printf("BeforeTest: 类别=%s\n", s.category)
}
func (s *CategorySuite) AfterTest(suiteName, testName string) {
fmt.Printf("AfterTest: %s 完成(类别:%s)\n", testName, s.category)
}
func (s *CategorySuite) TearDownSuite() {
fmt.Println("测试统计:")
for category, count := range s.metrics {
fmt.Printf(" %s: %d\n", category, count)
}
}
func (s *CategorySuite) TestUnitCalculation() {
s.Equal("unit", s.category)
}
func (s *CategorySuite) TestIntegrationDatabase() {
s.Equal("integration", s.category)
}
func (s *CategorySuite) TestSomething() {
s.Equal("other", s.category)
}
func TestCategorySuite(t *testing.T) {
suite.Run(t, new(CategorySuite))
}
// 完整生命周期演示
type FullLifecycleSuite struct {
suite.Suite
log []string
}
func (s *FullLifecycleSuite) SetupSuite() {
s.log = append(s.log, "1.SetupSuite")
}
func (s *FullLifecycleSuite) SetupTest() {
s.log = append(s.log, "2.SetupTest")
}
func (s *FullLifecycleSuite) BeforeTest(suiteName, testName string) {
s.log = append(s.log, "3.BeforeTest")
}
func (s *FullLifecycleSuite) TestExample() {
s.log = append(s.log, "4.TestExample")
// 验证执行顺序
s.Len(s.log, 4)
}
func (s *FullLifecycleSuite) AfterTest(suiteName, testName string) {
s.log = append(s.log, "5.AfterTest")
}
func (s *FullLifecycleSuite) TearDownTest() {
s.log = append(s.log, "6.TearDownTest")
}
func (s *FullLifecycleSuite) TearDownSuite() {
s.log = append(s.log, "7.TearDownSuite")
// 完整的执行顺序
expected := []string{
"1.SetupSuite",
"2.SetupTest",
"3.BeforeTest",
"4.TestExample",
"5.AfterTest",
"6.TearDownTest",
"7.TearDownSuite",
}
fmt.Printf("执行顺序: %v\n", expected)
}
func TestFullLifecycleSuite(t *testing.T) {
suite.Run(t, new(FullLifecycleSuite))
}
---
04.生命周期最佳实践
a.选择合适的钩子
SetupSuite用于昂贵的、可共享的初始化如数据库连接。SetupTest用于需要隔离的、每次测试都要重置的状态。BeforeTest用于需要根据测试名称定制的逻辑。避免在错误的钩子中执行操作导致性能问题或测试污染。
b.错误处理
Setup方法中的错误应该使用s.T().Fatal()或s.Fail()报告,这会终止测试。TearDown方法应该尽可能健壮,即使出错也要尝试清理资源。使用defer可以确保清理代码一定执行。
c.代码示例
---
// 生命周期最佳实践
package lifecycle best
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/suite"
)
type BestPracticeSuite struct {
suite.Suite
expensiveResource interface{}
testResource interface{}
}
// ✅ 正确:昂贵资源在SetupSuite
func (s *BestPracticeSuite) SetupSuite() {
var err error
s.expensiveResource, err = initializeExpensiveResource()
if err != nil {
s.T().Fatalf("初始化失败: %v", err)
}
}
// ✅ 正确:测试级资源在SetupTest
func (s *BestPracticeSuite) SetupTest() {
s.testResource = createTestResource()
}
// ✅ 正确:清理使用defer确保执行
func (s *BestPracticeSuite) TearDownTest() {
if s.testResource != nil {
cleanupTestResource(s.testResource)
s.testResource = nil
}
}
// ✅ 正确:健壮的TearDown
func (s *BestPracticeSuite) TearDownSuite() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("TearDown panic recovered: %v\n", r)
}
}()
if s.expensiveResource != nil {
cleanupExpensiveResource(s.expensiveResource)
}
}
func (s *BestPracticeSuite) TestSomething() {
s.NotNil(s.expensiveResource)
s.NotNil(s.testResource)
}
func TestBestPracticeSuite(t *testing.T) {
suite.Run(t, new(BestPracticeSuite))
}
// ❌ 反面示例:错误的资源管理
type BadPracticeSuite struct {
suite.Suite
sharedState int
}
// ❌ 错误:在SetupSuite修改会在测试间共享的状态
func (s *BadPracticeSuite) SetupSuite() {
s.sharedState = 0 // 所有测试共享此状态
}
func (s *BadPracticeSuite) TestOne() {
s.sharedState++ // 修改共享状态
s.Equal(1, s.sharedState)
}
func (s *BadPracticeSuite) TestTwo() {
s.sharedState++ // 依赖前一个测试的状态(坏的)
// s.Equal(1, s.sharedState) // 可能失败!
}
// ✅ 正确的版本
type GoodPracticeSuite struct {
suite.Suite
testState int
}
func (s *GoodPracticeSuite) SetupTest() {
s.testState = 0 // 每个测试独立初始化
}
func (s *GoodPracticeSuite) TestOne() {
s.testState++
s.Equal(1, s.testState)
}
func (s *GoodPracticeSuite) TestTwo() {
s.testState++
s.Equal(1, s.testState) // 总是通过
}
func TestGoodPracticeSuite(t *testing.T) {
suite.Run(t, new(GoodPracticeSuite))
}
// 辅助函数
func initializeExpensiveResource() (interface{}, error) {
// 模拟昂贵的初始化
return "expensive", nil
}
func cleanupExpensiveResource(resource interface{}) {
// 清理
}
func createTestResource() interface{} {
return "test"
}
func cleanupTestResource(resource interface{}) {
// 清理
}
---
5.3 子测试
01.Run方法创建子测试
a.子测试概念
Suite中可以使用s.Run()创建子测试,类似于testing包的t.Run()。子测试允许在一个测试方法内组织多个相关的测试场景。每个子测试有独立的名称,失败时可以准确定位。子测试支持并发执行和独立的setup/teardown。
b.Run方法语法
s.Run接收测试名称和函数。函数没有参数,在其中可以使用Suite的所有断言方法。子测试可以嵌套,创建层级结构。子测试的名称会包含父测试的名称,形成完整的测试路径。
c.代码示例
---
// 子测试基础
package subtest
import (
"testing"
"github.com/stretchr/testify/suite"
)
type SubTestSuite struct {
suite.Suite
}
func (s *SubTestSuite) TestWithSubTests() {
// 子测试1
s.Run("positive numbers", func() {
s.Greater(10, 5)
s.Greater(100, 50)
})
// 子测试2
s.Run("negative numbers", func() {
s.Less(-5, 0)
s.Less(-100, -10)
})
// 子测试3
s.Run("zero", func() {
s.Equal(0, 0)
s.Zero(0)
})
}
func (s *SubTestSuite) TestNestedSubTests() {
s.Run("arithmetic", func() {
s.Run("addition", func() {
s.Equal(5, 2+3)
s.Equal(10, 7+3)
})
s.Run("subtraction", func() {
s.Equal(2, 5-3)
s.Equal(0, 3-3)
})
})
s.Run("comparison", func() {
s.Run("equality", func() {
s.Equal(1, 1)
s.Equal("a", "a")
})
s.Run("inequality", func() {
s.NotEqual(1, 2)
s.NotEqual("a", "b")
})
})
}
func TestSubTestSuite(t *testing.T) {
suite.Run(t, new(SubTestSuite))
}
// 表格驱动测试with子测试
type CalculatorSuite struct {
suite.Suite
}
func (s *CalculatorSuite) TestAddition() {
testCases := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -2, -3, -5},
{"mixed", 10, -5, 5},
{"zero", 0, 5, 5},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
result := tc.a + tc.b
s.Equal(tc.expected, result)
})
}
}
func (s *CalculatorSuite) TestDivision() {
s.Run("normal division", func() {
s.Equal(2, 10/5)
s.Equal(3, 9/3)
})
s.Run("division by zero", func() {
// 注意:Go中整数除以0会panic
s.Panics(func() {
_ = 10 / 0
})
})
s.Run("float division", func() {
s.InDelta(3.333, 10.0/3.0, 0.001)
})
}
func TestCalculatorSuite(t *testing.T) {
suite.Run(t, new(CalculatorSuite))
}
---
02.子测试数据隔离
a.子测试的独立性
每个子测试在执行时是独立的,但它们共享父测试方法的作用域。子测试内的变量修改不影响其他子测试(如果是值类型),但共享的引用类型需要注意。可以为每个子测试创建独立的数据副本。
b.数据准备模式
在子测试循环中,应该将循环变量赋值给局部变量,避免闭包陷阱。对于复杂数据,可以在每个子测试开始时clone或重新创建。这确保子测试间完全隔离,不会相互干扰。
c.代码示例
---
// 子测试数据隔离
package subisolation
import (
"testing"
"github.com/stretchr/testify/suite"
)
type IsolationSuite struct {
suite.Suite
}
// ✅ 正确:值类型隔离
func (s *IsolationSuite) TestValueTypeIsolation() {
counter := 0
s.Run("increment once", func() {
counter++
s.Equal(1, counter)
})
s.Run("increment again", func() {
counter++
// counter从上一个子测试的结果继续
s.Equal(2, counter)
})
}
// ⚠️ 注意:引用类型需要小心
func (s *IsolationSuite) TestReferenceTypeSharing() {
shared := []int{1, 2, 3}
s.Run("modify slice", func() {
shared = append(shared, 4)
s.Len(shared, 4)
})
s.Run("slice is modified", func() {
// shared仍然是修改后的版本
s.Len(shared, 4)
})
}
// ✅ 正确:为每个子测试创建独立数据
func (s *IsolationSuite) TestWithFreshData() {
testCases := []struct {
name string
data []int
}{
{"case1", []int{1, 2, 3}},
{"case2", []int{4, 5, 6}},
{"case3", []int{7, 8, 9}},
}
for _, tc := range testCases {
// 创建局部变量(重要!)
tc := tc
s.Run(tc.name, func() {
// 每个子测试使用独立的数据副本
localData := make([]int, len(tc.data))
copy(localData, tc.data)
// 修改localData不影响其他子测试
localData[0] = 999
s.Equal(999, localData[0])
})
}
}
// ✅ 正确:避免闭包陷阱
func (s *IsolationSuite) TestClosureTrap() {
inputs := []int{1, 2, 3, 4, 5}
// ❌ 错误的方式
for i, val := range inputs {
s.Run("wrong", func() {
// val和i是共享的,可能不是预期的值
_ = val
_ = i
})
}
// ✅ 正确的方式
for i, val := range inputs {
i, val := i, val // 创建局部副本
s.Run("correct", func() {
s.Less(i, len(inputs))
s.Equal(inputs[i], val)
})
}
}
func TestIsolationSuite(t *testing.T) {
suite.Run(t, new(IsolationSuite))
}
// 实际应用:用户数据测试
type UserTestSuite struct {
suite.Suite
}
type User struct {
ID int
Name string
Age int
}
func (s *UserTestSuite) TestUserValidation() {
users := []User{
{ID: 1, Name: "Alice", Age: 25},
{ID: 2, Name: "Bob", Age: 30},
{ID: 3, Name: "", Age: 20}, // invalid
{ID: 4, Name: "David", Age: 0}, // invalid
}
for _, user := range users {
user := user // 局部副本
s.Run(user.Name, func() {
if user.Name == "" {
s.Empty(user.Name)
} else {
s.NotEmpty(user.Name)
}
if user.Age == 0 {
s.Zero(user.Age)
} else {
s.Positive(user.Age)
}
})
}
}
func TestUserTestSuite(t *testing.T) {
suite.Run(t, new(UserTestSuite))
}
---
03.子测试组织策略
a.按场景分组
使用子测试将相关场景组织在一起。例如成功场景、失败场景、边界条件等。每个主测试方法关注一个功能点,子测试覆盖该功能的不同方面。这使测试结构清晰,易于理解和维护。
b.表格驱动模式
子测试与表格驱动测试完美结合。定义测试用例结构体数组,为每个用例创建子测试。用例包含输入、期望输出、测试名称等。这种模式既简洁又强大,是Go测试的最佳实践。
c.代码示例
---
// 子测试组织策略
package suborganization
import (
"errors"
"testing"
"github.com/stretchr/testify/suite"
)
type OrganizationSuite struct {
suite.Suite
}
// 策略1:按场景分组
func (s *OrganizationSuite) TestEmailValidation() {
validator := &EmailValidator{}
s.Run("valid emails", func() {
s.Run("simple", func() {
s.True(validator.IsValid("[email protected]"))
})
s.Run("with subdomain", func() {
s.True(validator.IsValid("[email protected]"))
})
s.Run("with plus", func() {
s.True(validator.IsValid("[email protected]"))
})
})
s.Run("invalid emails", func() {
s.Run("no at symbol", func() {
s.False(validator.IsValid("userexample.com"))
})
s.Run("no domain", func() {
s.False(validator.IsValid("user@"))
})
s.Run("no username", func() {
s.False(validator.IsValid("@example.com"))
})
})
s.Run("edge cases", func() {
s.Run("empty string", func() {
s.False(validator.IsValid(""))
})
s.Run("whitespace", func() {
s.False(validator.IsValid(" "))
})
})
}
// 策略2:表格驱动
func (s *OrganizationSuite) TestPasswordStrength() {
checker := &PasswordChecker{}
testCases := []struct {
name string
password string
expected string
}{
{"very strong", "Abcd1234!@#$", "strong"},
{"strong", "Password123!", "strong"},
{"medium", "password123", "medium"},
{"weak", "password", "weak"},
{"very weak", "12345", "weak"},
{"empty", "", "weak"},
}
for _, tc := range testCases {
tc := tc
s.Run(tc.name, func() {
result := checker.CheckStrength(tc.password)
s.Equal(tc.expected, result)
})
}
}
// 策略3:功能分解
func (s *OrganizationSuite) TestUserRegistration() {
service := &UserService{}
s.Run("input validation", func() {
s.Run("valid input", func() {
err := service.ValidateInput("[email protected]", "Pass123!")
s.NoError(err)
})
s.Run("invalid email", func() {
err := service.ValidateInput("invalid", "Pass123!")
s.Error(err)
})
s.Run("weak password", func() {
err := service.ValidateInput("[email protected]", "123")
s.Error(err)
})
})
s.Run("user creation", func() {
s.Run("success", func() {
user, err := service.CreateUser("[email protected]", "Pass123!")
s.NoError(err)
s.NotNil(user)
})
s.Run("duplicate email", func() {
_, err := service.CreateUser("[email protected]", "Pass123!")
s.Error(err)
})
})
s.Run("notification", func() {
s.Run("welcome email sent", func() {
// 测试邮件发送逻辑
s.True(true)
})
})
}
// 策略4:错误场景覆盖
func (s *OrganizationSuite) TestFileOperation() {
fileOps := &FileOperations{}
s.Run("success scenarios", func() {
s.Run("read existing file", func() {
data, err := fileOps.Read("/valid/path.txt")
s.NoError(err)
s.NotNil(data)
})
s.Run("write to writable location", func() {
err := fileOps.Write("/writable/file.txt", []byte("data"))
s.NoError(err)
})
})
s.Run("error scenarios", func() {
s.Run("file not found", func() {
_, err := fileOps.Read("/nonexistent/file.txt")
s.Error(err)
})
s.Run("permission denied", func() {
err := fileOps.Write("/readonly/file.txt", []byte("data"))
s.Error(err)
})
s.Run("disk full", func() {
err := fileOps.Write("/full/file.txt", make([]byte, 1e9))
s.Error(err)
})
})
}
func TestOrganizationSuite(t *testing.T) {
suite.Run(t, new(OrganizationSuite))
}
// 辅助类型
type EmailValidator struct{}
func (v *EmailValidator) IsValid(email string) bool {
return len(email) > 3 && email[0] != '@' && email[len(email)-1] != '@'
}
type PasswordChecker struct{}
func (c *PasswordChecker) CheckStrength(password string) string {
if len(password) >= 12 {
return "strong"
} else if len(password) >= 8 {
return "medium"
}
return "weak"
}
type UserService struct{}
func (s *UserService) ValidateInput(email, password string) error {
if len(email) < 3 {
return errors.New("invalid email")
}
if len(password) < 6 {
return errors.New("weak password")
}
return nil
}
func (s *UserService) CreateUser(email, password string) (*User, error) {
if email == "[email protected]" {
return nil, errors.New("duplicate email")
}
return &User{Name: email}, nil
}
type FileOperations struct{}
func (f *FileOperations) Read(path string) ([]byte, error) {
if path == "/nonexistent/file.txt" {
return nil, errors.New("not found")
}
return []byte("data"), nil
}
func (f *FileOperations) Write(path string, data []byte) error {
if path == "/readonly/file.txt" {
return errors.New("permission denied")
}
if len(data) > 1e6 {
return errors.New("disk full")
}
return nil
}
---
5.4 并发测试
01.并发测试基础
a.t.Parallel()使用
在Suite的测试方法中调用s.T().Parallel()可以让该测试与其他并发测试并行执行。这可以大幅减少测试套件的总执行时间。并发测试之间必须完全独立,不能共享可变状态。注意Parallel标记的测试会等到所有非并发测试完成后才开始。
b.并发安全性
并发测试要求被测代码和测试代码都是并发安全的。共享的Suite字段如果在测试中被修改,会导致data race。使用go test -race可以检测并发问题。确保每个并发测试有独立的数据和资源。
c.代码示例
---
// 并发测试基础
package parallel
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type ParallelSuite struct {
suite.Suite
// 只读字段是安全的
config string
}
func (s *ParallelSuite) SetupSuite() {
s.config = "shared config"
}
// 并发测试1
func (s *ParallelSuite) TestParallel1() {
s.T().Parallel()
time.Sleep(100 * time.Millisecond)
s.Equal("shared config", s.config)
}
// 并发测试2
func (s *ParallelSuite) TestParallel2() {
s.T().Parallel()
time.Sleep(100 * time.Millisecond)
s.NotEmpty(s.config)
}
// 并发测试3
func (s *ParallelSuite) TestParallel3() {
s.T().Parallel()
time.Sleep(100 * time.Millisecond)
s.True(true)
}
func TestParallelSuite(t *testing.T) {
suite.Run(t, new(ParallelSuite))
}
// 并发安全的测试
type ConcurrentSafeSuite struct {
suite.Suite
}
func (s *ConcurrentSafeSuite) TestConcurrentOperation1() {
s.T().Parallel()
// 每个测试有独立的数据
data := []int{1, 2, 3}
sum := 0
for _, v := range data {
sum += v
}
s.Equal(6, sum)
}
func (s *ConcurrentSafeSuite) TestConcurrentOperation2() {
s.T().Parallel()
// 独立的数据
data := map[string]int{
"a": 1,
"b": 2,
}
s.Len(data, 2)
}
func TestConcurrentSafeSuite(t *testing.T) {
suite.Run(t, new(ConcurrentSafeSuite))
}
// ⚠️ 不安全的并发测试示例
type UnsafeSuite struct {
suite.Suite
counter int // 共享可变状态
}
// ❌ 这会产生data race
func (s *UnsafeSuite) TestUnsafe1() {
s.T().Parallel()
s.counter++ // race condition
time.Sleep(10 * time.Millisecond)
}
func (s *UnsafeSuite) TestUnsafe2() {
s.T().Parallel()
s.counter++ // race condition
time.Sleep(10 * time.Millisecond)
}
// ✅ 正确的版本:使用局部变量
type SafeSuite struct {
suite.Suite
}
func (s *SafeSuite) TestSafe1() {
s.T().Parallel()
counter := 0
counter++
s.Equal(1, counter)
}
func (s *SafeSuite) TestSafe2() {
s.T().Parallel()
counter := 0
counter++
s.Equal(1, counter)
}
func TestSafeSuite(t *testing.T) {
suite.Run(t, new(SafeSuite))
}
---
02.测试并发代码
a.goroutine测试
测试包含goroutine的代码需要使用WaitGroup或channel等同步机制确保goroutine完成。可以使用有缓冲channel收集goroutine的结果。设置超时避免测试hang住。使用t.Parallel可以并发运行多个这样的测试。
b.竞态条件检测
使用go test -race运行测试可以检测data race。testify的断言不是并发安全的,不要在多个goroutine中同时调用。如果需要在goroutine中断言,使用channel收集结果,在主goroutine中断言。
c.代码示例
---
// 测试并发代码
package concurrent
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type ConcurrentCodeSuite struct {
suite.Suite
}
// 测试goroutine
func (s *ConcurrentCodeSuite) TestGoroutineExecution() {
var wg sync.WaitGroup
results := make([]int, 0)
mu := sync.Mutex{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
mu.Lock()
results = append(results, n*2)
mu.Unlock()
}(i)
}
wg.Wait()
s.Len(results, 10)
}
// 测试channel通信
func (s *ConcurrentCodeSuite) TestChannelCommunication() {
ch := make(chan int, 10)
// 生产者
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
// 消费者
count := 0
for range ch {
count++
}
s.Equal(10, count)
}
// 测试超时
func (s *ConcurrentCodeSuite) TestWithTimeout() {
done := make(chan bool)
go func() {
time.Sleep(50 * time.Millisecond)
done <- true
}()
select {
case <-done:
s.True(true, "完成")
case <-time.After(100 * time.Millisecond):
s.Fail("超时")
}
}
// ✅ 正确:在主goroutine中断言
func (s *ConcurrentCodeSuite) TestAssertionInMainGoroutine() {
results := make(chan int, 10)
// worker goroutines
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
results <- n * n
}(i)
}
// 等待并关闭channel
go func() {
wg.Wait()
close(results)
}()
// 在主goroutine中收集结果并断言
sum := 0
for result := range results {
sum += result
}
s.Equal(285, sum) // 0^2+1^2+...+9^2 = 285
}
// 测试并发安全的结构
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Inc() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func (s *ConcurrentCodeSuite) TestSafeCounter() {
counter := &SafeCounter{}
var wg sync.WaitGroup
// 并发增加计数
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Inc()
}()
}
wg.Wait()
s.Equal(1000, counter.Value())
}
func TestConcurrentCodeSuite(t *testing.T) {
suite.Run(t, new(ConcurrentCodeSuite))
}
---
03.并发性能测试
a.压力测试
使用大量并发goroutine测试系统在高负载下的行为。验证没有data race、死锁、资源泄漏等问题。可以逐渐增加并发数,观察系统表现。结合-race标志和监控工具检测问题。
b.吞吐量测试
测试并发操作的吞吐量,统计单位时间内完成的操作数。使用WaitGroup和time.Since测量执行时间。比较串行和并发版本的性能差异。这类测试帮助优化并发策略。
c.代码示例
---
// 并发性能测试
package performance
import (
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type PerformanceSuite struct {
suite.Suite
}
// 压力测试
func (s *PerformanceSuite) TestHighConcurrency() {
const goroutines = 1000
const iterations = 100
var counter int64
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
atomic.AddInt64(&counter, 1)
}
}()
}
wg.Wait()
duration := time.Since(start)
expected := int64(goroutines * iterations)
s.Equal(expected, counter)
s.T().Logf("完成 %d 操作,耗时 %v", counter, duration)
}
// 吞吐量测试
func (s *PerformanceSuite) TestThroughput() {
const duration = 1 * time.Second
const workers = 10
var ops int64
stop := make(chan bool)
var wg sync.WaitGroup
// 启动workers
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
// 模拟操作
time.Sleep(time.Microsecond)
atomic.AddInt64(&ops, 1)
}
}
}()
}
// 运行指定时间
time.Sleep(duration)
close(stop)
wg.Wait()
s.T().Logf("吞吐量: %d ops/s", ops)
s.Greater(ops, int64(0))
}
// 并发vs串行对比
func (s *PerformanceSuite) TestConcurrentVsSerial() {
const tasks = 100
// 串行执行
startSerial := time.Now()
for i := 0; i < tasks; i++ {
time.Sleep(time.Millisecond)
}
serialDuration := time.Since(startSerial)
// 并发执行
startConcurrent := time.Now()
var wg sync.WaitGroup
for i := 0; i < tasks; i++ {
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(time.Millisecond)
}()
}
wg.Wait()
concurrentDuration := time.Since(startConcurrent)
s.T().Logf("串行: %v, 并发: %v", serialDuration, concurrentDuration)
s.Less(concurrentDuration, serialDuration)
}
// 资源池测试
type ResourcePool struct {
resources chan *Resource
}
type Resource struct {
ID int
}
func NewResourcePool(size int) *ResourcePool {
pool := &ResourcePool{
resources: make(chan *Resource, size),
}
for i := 0; i < size; i++ {
pool.resources <- &Resource{ID: i}
}
return pool
}
func (p *ResourcePool) Acquire() *Resource {
return <-p.resources
}
func (p *ResourcePool) Release(r *Resource) {
p.resources <- r
}
func (s *PerformanceSuite) TestResourcePool() {
pool := NewResourcePool(10)
const clients = 100
var wg sync.WaitGroup
for i := 0; i < clients; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 获取资源
resource := pool.Acquire()
s.NotNil(resource)
// 使用资源
time.Sleep(10 * time.Millisecond)
// 释放资源
pool.Release(resource)
}()
}
wg.Wait()
s.Len(pool.resources, 10)
}
func TestPerformanceSuite(t *testing.T) {
suite.Run(t, new(PerformanceSuite))
}
---
5.5 测试夹具
01.夹具概念
a.什么是测试夹具
测试夹具(Fixture)是测试运行所需的固定状态和环境,包括测试数据、Mock对象、数据库连接、配置等。在Suite中,夹具通常在Setup方法中创建,在TearDown方法中清理。良好的夹具管理使测试可靠、可重复、易维护。
b.夹具的作用
夹具提供测试所需的已知初始状态。消除测试间的依赖,确保每个测试独立运行。减少重复代码,Setup中的初始化逻辑被所有测试共享。夹具是测试可预测性的基础。
c.代码示例
---
// 测试夹具基础
package fixture
import (
"testing"
"github.com/stretchr/testify/suite"
)
// 基础夹具示例
type BasicFixtureSuite struct {
suite.Suite
// 夹具字段
testUser *User
testData []Item
mockDB *MockDatabase
}
type User struct {
ID int
Name string
}
type Item struct {
Name string
Value int
}
type MockDatabase struct {
data map[string]interface{}
}
// 设置夹具
func (s *BasicFixtureSuite) SetupTest() {
// 创建测试用户
s.testUser = &User{
ID: 1,
Name: "TestUser",
}
// 准备测试数据
s.testData = []Item{
{Name: "item1", Value: 10},
{Name: "item2", Value: 20},
{Name: "item3", Value: 30},
}
// 初始化Mock
s.mockDB = &MockDatabase{
data: make(map[string]interface{}),
}
}
// 清理夹具
func (s *BasicFixtureSuite) TearDownTest() {
s.testUser = nil
s.testData = nil
s.mockDB = nil
}
// 测试使用夹具
func (s *BasicFixtureSuite) TestUserFixture() {
s.NotNil(s.testUser)
s.Equal("TestUser", s.testUser.Name)
}
func (s *BasicFixtureSuite) TestDataFixture() {
s.Len(s.testData, 3)
s.Equal(10, s.testData[0].Value)
}
func (s *BasicFixtureSuite) TestMockFixture() {
s.mockDB.data["key"] = "value"
s.Equal("value", s.mockDB.data["key"])
}
func TestBasicFixtureSuite(t *testing.T) {
suite.Run(t, new(BasicFixtureSuite))
}
---
02.共享夹具
a.Suite级夹具
在SetupSuite中创建的夹具被所有测试共享。适用于昂贵的初始化操作如数据库连接、大文件加载等。共享夹具必须是只读的或并发安全的,避免测试间相互影响。在TearDownSuite中清理。
b.测试级夹具
在SetupTest中创建的夹具为每个测试独立准备。适用于可变状态、需要隔离的资源。每个测试获得新鲜的夹具实例,修改不影响其他测试。在TearDownTest中清理。
c.代码示例
---
// 共享夹具管理
package sharedfixture
import (
"testing"
"github.com/stretchr/testify/suite"
)
type SharedFixtureSuite struct {
suite.Suite
// Suite级夹具:所有测试共享
config map[string]string
constants []string
// 测试级夹具:每个测试独立
counter int
buffer []byte
}
// Suite级初始化
func (s *SharedFixtureSuite) SetupSuite() {
// 只初始化一次
s.config = map[string]string{
"db_host": "localhost",
"db_port": "3306",
"api_key": "secret_key",
}
s.constants = []string{"ONE", "TWO", "THREE"}
}
// 测试级初始化
func (s *SharedFixtureSuite) SetupTest() {
// 每个测试都重新初始化
s.counter = 0
s.buffer = make([]byte, 0, 1024)
}
func (s *SharedFixtureSuite) TearDownTest() {
s.buffer = nil
}
func (s *SharedFixtureSuite) TestSharedConfig() {
// 可以读取共享配置
s.Equal("localhost", s.config["db_host"])
s.Len(s.constants, 3)
// 修改测试级夹具
s.counter = 10
s.buffer = append(s.buffer, 'a', 'b', 'c')
}
func (s *SharedFixtureSuite) TestIsolatedState() {
// 测试级夹具已重置
s.Equal(0, s.counter)
s.Len(s.buffer, 0)
// 共享夹具仍然可用
s.NotEmpty(s.config)
}
func TestSharedFixtureSuite(t *testing.T) {
suite.Run(t, new(SharedFixtureSuite))
}
// 实际应用:数据库夹具
type DatabaseFixtureSuite struct {
suite.Suite
// 共享连接
dbConnection *DBConnection
// 测试独立的事务
tx *Transaction
}
type DBConnection struct {
host string
}
type Transaction struct {
id int
}
func (s *DatabaseFixtureSuite) SetupSuite() {
// 建立数据库连接(昂贵操作)
s.dbConnection = &DBConnection{host: "localhost"}
}
func (s *DatabaseFixtureSuite) SetupTest() {
// 每个测试开始新事务
s.tx = &Transaction{id: 1}
}
func (s *DatabaseFixtureSuite) TearDownTest() {
// 回滚事务
s.tx = nil
}
func (s *DatabaseFixtureSuite) TearDownSuite() {
// 关闭连接
s.dbConnection = nil
}
func (s *DatabaseFixtureSuite) TestInsert() {
s.NotNil(s.dbConnection)
s.NotNil(s.tx)
// 在事务中执行插入
}
func (s *DatabaseFixtureSuite) TestQuery() {
s.NotNil(s.dbConnection)
s.NotNil(s.tx)
// 在事务中执行查询
}
func TestDatabaseFixtureSuite(t *testing.T) {
suite.Run(t, new(DatabaseFixtureSuite))
}
---
03.夹具工厂模式
a.工厂函数
使用工厂函数创建夹具,集中管理创建逻辑。工厂函数接收参数定制夹具。这使夹具创建灵活可复用,便于在不同测试中使用相同或相似的夹具。工厂模式也便于维护和修改夹具结构。
b.Builder模式
对于复杂夹具,使用Builder模式逐步构建。Builder提供流畅的API设置夹具的各个部分。最后调用Build()生成夹具。这种模式使夹具创建清晰且可定制。
c.代码示例
---
// 夹具工厂模式
package fixturefactory
import (
"testing"
"github.com/stretchr/testify/suite"
)
type FactorySuite struct {
suite.Suite
}
// 简单工厂函数
func NewTestUser(id int, name string) *User {
return &User{
ID: id,
Name: name,
}
}
func NewDefaultTestUser() *User {
return NewTestUser(1, "DefaultUser")
}
func (s *FactorySuite) TestWithFactory() {
user1 := NewTestUser(1, "Alice")
user2 := NewTestUser(2, "Bob")
user3 := NewDefaultTestUser()
s.Equal("Alice", user1.Name)
s.Equal("Bob", user2.Name)
s.Equal("DefaultUser", user3.Name)
}
// Builder模式
type OrderBuilder struct {
order *Order
}
type Order struct {
ID int
UserID int
Items []OrderItem
Total float64
Status string
}
type OrderItem struct {
Product string
Qty int
Price float64
}
func NewOrderBuilder() *OrderBuilder {
return &OrderBuilder{
order: &Order{
Items: make([]OrderItem, 0),
Status: "pending",
},
}
}
func (b *OrderBuilder) WithID(id int) *OrderBuilder {
b.order.ID = id
return b
}
func (b *OrderBuilder) WithUser(userID int) *OrderBuilder {
b.order.UserID = userID
return b
}
func (b *OrderBuilder) AddItem(product string, qty int, price float64) *OrderBuilder {
b.order.Items = append(b.order.Items, OrderItem{
Product: product,
Qty: qty,
Price: price,
})
b.order.Total += float64(qty) * price
return b
}
func (b *OrderBuilder) WithStatus(status string) *OrderBuilder {
b.order.Status = status
return b
}
func (b *OrderBuilder) Build() *Order {
return b.order
}
func (s *FactorySuite) TestWithBuilder() {
order := NewOrderBuilder().
WithID(100).
WithUser(1).
AddItem("Product1", 2, 10.0).
AddItem("Product2", 1, 20.0).
WithStatus("completed").
Build()
s.Equal(100, order.ID)
s.Equal(1, order.UserID)
s.Len(order.Items, 2)
s.Equal(40.0, order.Total)
s.Equal("completed", order.Status)
}
// 预定义夹具集
type TestData struct {
Users []*User
Orders []*Order
}
func NewTestDataSet() *TestData {
return &TestData{
Users: []*User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Charlie"},
},
Orders: []*Order{
{ID: 1, UserID: 1, Total: 100.0},
{ID: 2, UserID: 2, Total: 200.0},
},
}
}
func (s *FactorySuite) TestWithDataSet() {
data := NewTestDataSet()
s.Len(data.Users, 3)
s.Len(data.Orders, 2)
s.Equal("Alice", data.Users[0].Name)
}
func TestFactorySuite(t *testing.T) {
suite.Run(t, new(FactorySuite))
}
---
04.夹具最佳实践
a.最小化夹具
只创建测试真正需要的夹具,避免过度准备。大而全的夹具增加测试复杂度和维护成本。根据测试需求定制夹具。使用工厂或Builder创建不同大小的夹具。
b.夹具隔离
确保夹具间相互独立,一个测试的夹具修改不影响其他测试。避免全局状态和单例。使用SetupTest为每个测试创建新夹具。共享夹具必须是只读或并发安全的。
c.代码示例
---
// 夹具最佳实践
package fixturebest
import (
"testing"
"github.com/stretchr/testify/suite"
)
// ✅ 正确:最小化夹具
type MinimalFixtureSuite struct {
suite.Suite
}
func (s *MinimalFixtureSuite) TestUserName() {
// 只创建需要的数据
user := &User{Name: "Alice"}
s.Equal("Alice", user.Name)
}
func (s *MinimalFixtureSuite) TestUserID() {
// 这个测试只需要ID
user := &User{ID: 1}
s.Equal(1, user.ID)
}
func TestMinimalFixtureSuite(t *testing.T) {
suite.Run(t, new(MinimalFixtureSuite))
}
// ❌ 错误:过度准备
type OverpreparedSuite struct {
suite.Suite
complexData *ComplexData
}
type ComplexData struct {
Users []*User
Orders []*Order
Products []string
Config map[string]string
}
func (s *OverpreparedSuite) SetupTest() {
// 准备了大量数据,但每个测试可能只用一小部分
s.complexData = &ComplexData{
Users: make([]*User, 100),
Orders: make([]*Order, 200),
Products: make([]string, 50),
Config: make(map[string]string),
}
}
// ✅ 正确:按需创建
type OnDemandSuite struct {
suite.Suite
}
func (s *OnDemandSuite) createTestUsers(count int) []*User {
users := make([]*User, count)
for i := 0; i < count; i++ {
users[i] = &User{ID: i + 1}
}
return users
}
func (s *OnDemandSuite) TestWithFewUsers() {
users := s.createTestUsers(3)
s.Len(users, 3)
}
func (s *OnDemandSuite) TestWithManyUsers() {
users := s.createTestUsers(100)
s.Len(users, 100)
}
func TestOnDemandSuite(t *testing.T) {
suite.Run(t, new(OnDemandSuite))
}
// ✅ 正确:夹具清理
type CleanupSuite struct {
suite.Suite
tempFiles []string
}
func (s *CleanupSuite) SetupTest() {
s.tempFiles = make([]string, 0)
}
func (s *CleanupSuite) TearDownTest() {
// 清理临时文件
for _, file := range s.tempFiles {
// os.Remove(file)
_ = file
}
s.tempFiles = nil
}
func (s *CleanupSuite) TestFileCreation() {
file := "/tmp/test.txt"
s.tempFiles = append(s.tempFiles, file)
// 创建文件...
}
func TestCleanupSuite(t *testing.T) {
suite.Run(t, new(CleanupSuite))
}
// ✅ 正确:夹具隔离
type IsolatedFixtureSuite struct {
suite.Suite
}
func (s *IsolatedFixtureSuite) TestOne() {
// 每个测试创建自己的数据
data := map[string]int{"key": 1}
data["key"] = 10
s.Equal(10, data["key"])
}
func (s *IsolatedFixtureSuite) TestTwo() {
// 独立的数据,不受Test One影响
data := map[string]int{"key": 1}
s.Equal(1, data["key"])
}
func TestIsolatedFixtureSuite(t *testing.T) {
suite.Run(t, new(IsolatedFixtureSuite))
}
---
5.6 Suite组织策略
01.按功能模块组织
a.模块化Suite
为每个功能模块创建独立的Suite。每个Suite专注于测试一个明确的功能领域如用户管理、订单处理等。模块化使测试结构清晰,便于定位和维护。不同模块的Suite可以并行运行,提高测试效率。
b.命名规范
Suite名称应该清晰表达测试的模块或功能。使用XxxSuite命名格式,Xxx是模块或功能名。测试方法名应该描述具体的测试场景。好的命名使测试自文档化,易于理解测试意图。
c.代码示例
---
// 按功能模块组织Suite
package organization
import (
"testing"
"github.com/stretchr/testify/suite"
)
// 用户管理Suite
type UserManagementSuite struct {
suite.Suite
userService *UserService
}
type UserService struct{}
func (s *UserService) CreateUser(name string) *User {
return &User{Name: name}
}
func (s *UserService) DeleteUser(id int) bool {
return true
}
func (s *UserManagementSuite) SetupTest() {
s.userService = &UserService{}
}
func (s *UserManagementSuite) TestCreateUser() {
user := s.userService.CreateUser("Alice")
s.NotNil(user)
s.Equal("Alice", user.Name)
}
func (s *UserManagementSuite) TestDeleteUser() {
result := s.userService.DeleteUser(1)
s.True(result)
}
func TestUserManagementSuite(t *testing.T) {
suite.Run(t, new(UserManagementSuite))
}
// 订单处理Suite
type OrderProcessingSuite struct {
suite.Suite
orderService *OrderService
}
type OrderService struct{}
func (s *OrderService) CreateOrder(userID int) *Order {
return &Order{UserID: userID}
}
func (s *OrderService) CancelOrder(orderID int) error {
return nil
}
func (s *OrderProcessingSuite) SetupTest() {
s.orderService = &OrderService{}
}
func (s *OrderProcessingSuite) TestCreateOrder() {
order := s.orderService.CreateOrder(1)
s.NotNil(order)
s.Equal(1, order.UserID)
}
func (s *OrderProcessingSuite) TestCancelOrder() {
err := s.orderService.CancelOrder(1)
s.NoError(err)
}
func TestOrderProcessingSuite(t *testing.T) {
suite.Run(t, new(OrderProcessingSuite))
}
// 支付处理Suite
type PaymentProcessingSuite struct {
suite.Suite
paymentGateway *PaymentGateway
}
type PaymentGateway struct{}
func (g *PaymentGateway) ProcessPayment(amount float64) (string, error) {
return "tx_123", nil
}
func (s *PaymentProcessingSuite) SetupTest() {
s.paymentGateway = &PaymentGateway{}
}
func (s *PaymentProcessingSuite) TestProcessPayment() {
txID, err := s.paymentGateway.ProcessPayment(99.99)
s.NoError(err)
s.NotEmpty(txID)
}
func TestPaymentProcessingSuite(t *testing.T) {
suite.Run(t, new(PaymentProcessingSuite))
}
---
02.Suite继承与组合
a.基础Suite复用
创建包含通用设置的基础Suite,其他Suite通过嵌入复用。基础Suite可以提供通用的夹具、辅助方法、配置等。这减少代码重复,统一测试基础设施。子Suite可以覆盖基础Suite的方法定制行为。
b.组合多个Suite
一个测试文件可以定义多个相关的Suite,每个Suite测试不同的方面。Suite间可以共享类型定义和辅助函数。根据测试需求灵活组合,保持每个Suite专注且简洁。
c.代码示例
---
// Suite继承与组合
package inheritance
import (
"testing"
"github.com/stretchr/testify/suite"
)
// 基础Suite:提供通用功能
type BaseSuite struct {
suite.Suite
config map[string]string
}
func (s *BaseSuite) SetupSuite() {
s.config = map[string]string{
"env": "test",
"db_url": "localhost:3306",
}
}
func (s *BaseSuite) GetConfig(key string) string {
return s.config[key]
}
// 继承基础Suite的具体Suite
type DatabaseSuite struct {
BaseSuite
connection string
}
func (s *DatabaseSuite) SetupTest() {
s.connection = s.GetConfig("db_url")
}
func (s *DatabaseSuite) TestConnection() {
s.Equal("localhost:3306", s.connection)
s.NotEmpty(s.GetConfig("env"))
}
func TestDatabaseSuite(t *testing.T) {
suite.Run(t, new(DatabaseSuite))
}
// 另一个继承基础Suite的Suite
type APISuite struct {
BaseSuite
apiURL string
}
func (s *APISuite) SetupTest() {
env := s.GetConfig("env")
if env == "test" {
s.apiURL = "http://test.api.example.com"
}
}
func (s *APISuite) TestAPIURL() {
s.Equal("http://test.api.example.com", s.apiURL)
}
func TestAPISuite(t *testing.T) {
suite.Run(t, new(APISuite))
}
// 组合多个关注点
type IntegrationSuite struct {
suite.Suite
dbSuite *DatabaseSuite
apiSuite *APISuite
}
func (s *IntegrationSuite) SetupSuite() {
// 注意:这只是示例,实际中通常不这样嵌套Suite
s.dbSuite = &DatabaseSuite{}
s.apiSuite = &APISuite{}
}
func (s *IntegrationSuite) TestIntegration() {
s.NotNil(s.dbSuite)
s.NotNil(s.apiSuite)
}
func TestIntegrationSuite(t *testing.T) {
suite.Run(t, new(IntegrationSuite))
}
// 多层继承示例
type ServiceBaseSuite struct {
suite.Suite
timeout int
}
func (s *ServiceBaseSuite) SetupSuite() {
s.timeout = 30
}
type HTTPServiceSuite struct {
ServiceBaseSuite
client *HTTPClient
}
type HTTPClient struct{}
func (s *HTTPServiceSuite) SetupTest() {
s.client = &HTTPClient{}
}
func (s *HTTPServiceSuite) TestHTTPClient() {
s.NotNil(s.client)
s.Equal(30, s.timeout)
}
func TestHTTPServiceSuite(t *testing.T) {
suite.Run(t, new(HTTPServiceSuite))
}
---
03.大型项目组织
a.包级组织
将测试Suite按Go包组织,每个包的测试Suite放在对应的_test包中。保持测试代码与生产代码的结构对应。使用internal/testing包存放共享的测试辅助代码、Mock、夹具工厂等。
b.分层测试策略
区分单元测试Suite、集成测试Suite、端到端测试Suite。使用build tags区分不同类型的测试。单元测试快速且隔离,集成测试验证组件交互,E2E测试验证完整流程。分层使测试金字塔清晰。
c.代码示例
---
// 大型项目组织示例
package largeproject
import (
"testing"
"github.com/stretchr/testify/suite"
)
// 单元测试Suite
type UnitTestSuite struct {
suite.Suite
}
func (s *UnitTestSuite) TestBusinessLogic() {
// 纯业务逻辑测试,无外部依赖
result := calculateDiscount(100, 0.1)
s.Equal(90.0, result)
}
func calculateDiscount(price, rate float64) float64 {
return price * (1 - rate)
}
func TestUnitTestSuite(t *testing.T) {
suite.Run(t, new(UnitTestSuite))
}
// 集成测试Suite
// +build integration
type IntegrationTestSuite struct {
suite.Suite
dbConn *DBConnection
}
func (s *IntegrationTestSuite) SetupSuite() {
// 连接真实数据库
s.dbConn = &DBConnection{}
}
func (s *IntegrationTestSuite) TestDatabaseIntegration() {
// 测试与数据库的交互
s.NotNil(s.dbConn)
}
func TestIntegrationTestSuite(t *testing.T) {
// 使用: go test -tags=integration
suite.Run(t, new(IntegrationTestSuite))
}
// E2E测试Suite
// +build e2e
type E2ETestSuite struct {
suite.Suite
server *TestServer
}
type TestServer struct{}
func (s *E2ETestSuite) SetupSuite() {
// 启动完整的测试环境
s.server = &TestServer{}
}
func (s *E2ETestSuite) TestCompleteWorkflow() {
// 测试端到端流程
s.NotNil(s.server)
}
func TestE2ETestSuite(t *testing.T) {
// 使用: go test -tags=e2e
suite.Run(t, new(E2ETestSuite))
}
// 共享测试辅助
type TestHelper struct{}
func NewTestHelper() *TestHelper {
return &TestHelper{}
}
// 多Suite协同
type FeatureTestSuite struct {
suite.Suite
helper *TestHelper
}
func (s *FeatureTestSuite) SetupSuite() {
s.helper = NewTestHelper()
}
func (s *FeatureTestSuite) TestFeatureA() {
s.NotNil(s.helper)
}
func (s *FeatureTestSuite) TestFeatureB() {
s.NotNil(s.helper)
}
func TestFeatureTestSuite(t *testing.T) {
suite.Run(t, new(FeatureTestSuite))
}
---
04.Suite组织最佳实践
a.保持Suite专注
每个Suite应该有明确的测试范围,不要让Suite变得过大。大Suite难以维护和理解。当Suite超过15-20个测试方法时,考虑拆分。根据功能、场景或测试类型拆分Suite。
b.平衡粒度
Suite不宜过细也不宜过粗。过细导致大量小Suite,增加管理成本。过粗导致Suite臃肿,难以定位问题。找到适合项目的平衡点。通常一个文件包含1-3个相关Suite是合理的。
c.代码示例
---
// Suite组织最佳实践
package bestpractice
import (
"testing"
"github.com/stretchr/testify/suite"
)
// ✅ 正确:专注的Suite
type UserAuthenticationSuite struct {
suite.Suite
authService *AuthService
}
type AuthService struct{}
func (s *AuthService) Login(username, password string) bool {
return username != "" && password != ""
}
func (s *AuthService) Logout(sessionID string) bool {
return true
}
func (s *UserAuthenticationSuite) SetupTest() {
s.authService = &AuthService{}
}
func (s *UserAuthenticationSuite) TestSuccessfulLogin() {
result := s.authService.Login("user", "pass")
s.True(result)
}
func (s *UserAuthenticationSuite) TestFailedLogin() {
result := s.authService.Login("", "")
s.False(result)
}
func (s *UserAuthenticationSuite) TestLogout() {
result := s.authService.Logout("session123")
s.True(result)
}
func TestUserAuthenticationSuite(t *testing.T) {
suite.Run(t, new(UserAuthenticationSuite))
}
// ✅ 正确:相关功能的独立Suite
type UserProfileSuite struct {
suite.Suite
profileService *ProfileService
}
type ProfileService struct{}
func (s *ProfileService) GetProfile(userID int) *Profile {
return &Profile{UserID: userID}
}
type Profile struct {
UserID int
}
func (s *UserProfileSuite) SetupTest() {
s.profileService = &ProfileService{}
}
func (s *UserProfileSuite) TestGetProfile() {
profile := s.profileService.GetProfile(1)
s.NotNil(profile)
}
func TestUserProfileSuite(t *testing.T) {
suite.Run(t, new(UserProfileSuite))
}
// ❌ 错误:过大的Suite(反面示例)
type EverythingSuite struct {
suite.Suite
userService *UserService
orderService *OrderService
paymentGateway *PaymentGateway
emailService *EmailService
authService *AuthService
// ... 太多依赖
}
type EmailService struct{}
// 太多不相关的测试方法
func (s *EverythingSuite) TestUserCreation() {}
func (s *EverythingSuite) TestUserDeletion() {}
func (s *EverythingSuite) TestOrderProcessing() {}
func (s *EverythingSuite) TestPaymentProcessing() {}
func (s *EverythingSuite) TestEmailSending() {}
func (s *EverythingSuite) TestAuthentication() {}
// ... 20+ more methods
// ✅ 正确:清晰的文件组织
// user_auth_test.go -> UserAuthenticationSuite
// user_profile_test.go -> UserProfileSuite
// order_processing_test.go -> OrderProcessingSuite
// payment_test.go -> PaymentProcessingSuite
// ✅ 正确:使用子测试组织复杂场景
type ValidationSuite struct {
suite.Suite
}
func (s *ValidationSuite) TestEmailValidation() {
s.Run("valid emails", func() {
// 多个有效邮箱测试
})
s.Run("invalid emails", func() {
// 多个无效邮箱测试
})
}
func (s *ValidationSuite) TestPasswordValidation() {
s.Run("strong passwords", func() {
// 强密码测试
})
s.Run("weak passwords", func() {
// 弱密码测试
})
}
func TestValidationSuite(t *testing.T) {
suite.Run(t, new(ValidationSuite))
}
---
6 高级特性
6.1 HTTP测试工具
01.http包简介
a.testify/http功能
testify的http包提供了HTTP测试工具,包括TestResponseRecorder用于记录HTTP响应。可以轻松测试HTTP handler而无需启动真实服务器。支持验证响应状态码、响应头、响应体等。这使HTTP层测试变得简单高效。
b.与httptest的关系
testify的http包是对标准库httptest的补充和增强。提供了更友好的断言API。可以与httptest.ResponseRecorder配合使用。通常两者结合使用能覆盖大部分HTTP测试需求。
c.代码示例
---
// HTTP测试工具基础
package httptest
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
// 简单的HTTP handler
func HelloHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, World!"))
}
func TestHelloHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/hello", nil)
rec := httptest.NewRecorder()
HelloHandler(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "Hello, World!", rec.Body.String())
}
// JSON响应handler
func JSONHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message":"success"}`))
}
func TestJSONHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/api/data", nil)
rec := httptest.NewRecorder()
JSONHandler(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
assert.Contains(t, rec.Body.String(), "success")
}
// 带参数的handler
func EchoHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("name parameter required"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, " + name))
}
func TestEchoHandler(t *testing.T) {
t.Run("with name", func(t *testing.T) {
req := httptest.NewRequest("GET", "/echo?name=Alice", nil)
rec := httptest.NewRecorder()
EchoHandler(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "Hello, Alice", rec.Body.String())
})
t.Run("without name", func(t *testing.T) {
req := httptest.NewRequest("GET", "/echo", nil)
rec := httptest.NewRecorder()
EchoHandler(rec, req)
assert.Equal(t, http.StatusBadRequest, rec.Code)
assert.Contains(t, rec.Body.String(), "required")
})
}
// POST请求测试
func CreateHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusCreated)
w.Write([]byte("created"))
}
func TestCreateHandler(t *testing.T) {
req := httptest.NewRequest("POST", "/create", nil)
rec := httptest.NewRecorder()
CreateHandler(rec, req)
assert.Equal(t, http.StatusCreated, rec.Code)
assert.Equal(t, "created", rec.Body.String())
}
---
02.测试HTTP客户端
a.Mock HTTP服务器
使用httptest.NewServer创建测试HTTP服务器。服务器在随机端口监听,返回预定义的响应。测试完成后记得关闭服务器。这种方式可以测试HTTP客户端代码而不依赖真实服务。
b.请求验证
在测试服务器的handler中可以验证请求的method、headers、body等。记录收到的请求,在测试中断言。这确保HTTP客户端发送了正确的请求。
c.代码示例
---
// 测试HTTP客户端
package httpclient
import (
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// HTTP客户端
type APIClient struct {
baseURL string
client *http.Client
}
func NewAPIClient(baseURL string) *APIClient {
return &APIClient{
baseURL: baseURL,
client: &http.Client{},
}
}
func (c *APIClient) GetUser(id string) (string, error) {
resp, err := c.client.Get(c.baseURL + "/users/" + id)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
// 测试HTTP客户端
func TestAPIClient_GetUser(t *testing.T) {
// 创建测试服务器
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/users/123", r.URL.Path)
assert.Equal(t, "GET", r.Method)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"id":"123","name":"Alice"}`))
}))
defer server.Close()
// 创建客户端,指向测试服务器
client := NewAPIClient(server.URL)
// 执行请求
result, err := client.GetUser("123")
require.NoError(t, err)
assert.Contains(t, result, "Alice")
}
// 测试错误场景
func TestAPIClient_ServerError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("server error"))
}))
defer server.Close()
client := NewAPIClient(server.URL)
result, err := client.GetUser("999")
require.NoError(t, err)
assert.Contains(t, result, "error")
}
// 测试超时
func TestAPIClient_Timeout(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 模拟慢速响应
// time.Sleep(2 * time.Second)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
client := NewAPIClient(server.URL)
_, err := client.GetUser("1")
// 这个示例不会超时,实际测试需要设置超时
assert.NoError(t, err)
}
---
6.2 表格驱动测试
01.表格驱动模式
a.基本概念
表格驱动测试使用数据结构数组定义测试用例,每个用例包含输入和期望输出。遍历数组,对每个用例执行相同的测试逻辑。这种模式减少重复代码,使测试用例清晰可见,易于添加新用例。
b.优势
测试逻辑和测试数据分离,提高可维护性。添加新测试用例只需添加数据,无需重复代码。测试用例一目了然,便于review和理解。适合测试相同逻辑的多种输入组合。
c.代码示例
---
// 表格驱动测试基础
package tabledriven
import (
"testing"
"github.com/stretchr/testify/assert"
)
// 被测函数
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"mixed sign", 10, -5, 5},
{"zero", 0, 0, 0},
{"with zero", 5, 0, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
})
}
}
// 复杂测试用例
func ValidateEmail(email string) bool {
return len(email) > 3 && email[0] != '@'
}
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
expected bool
}{
{"valid email", "[email protected]", true},
{"valid with subdomain", "[email protected]", true},
{"invalid - no @", "userexample.com", true},
{"invalid - starts with @", "@example.com", false},
{"invalid - empty", "", false},
{"invalid - too short", "a@b", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ValidateEmail(tt.email)
assert.Equal(t, tt.expected, result,
"ValidateEmail(%q) should be %v", tt.email, tt.expected)
})
}
}
// 带错误的测试用例
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, assert.AnError
}
return a / b, nil
}
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b int
expectedVal int
expectError bool
}{
{"normal division", 10, 2, 5, false},
{"division by zero", 10, 0, 0, true},
{"negative result", -10, 2, -5, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Divide(tt.a, tt.b)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedVal, result)
}
})
}
}
---
02.与Suite结合
a.Suite中的表格驱动
在testify Suite中使用表格驱动测试结合了两者的优势。Suite提供setup/teardown和共享状态,表格驱动提供清晰的测试用例组织。在Suite的测试方法中定义测试表格并遍历。
b.共享夹具访问
表格驱动测试中的子测试可以访问Suite的字段和方法。可以在setup中准备数据,在表格用例中使用。这避免了在每个用例中重复初始化。
c.代码示例
---
// Suite与表格驱动结合
package suitetable
import (
"testing"
"github.com/stretchr/testify/suite"
)
type CalculatorSuite struct {
suite.Suite
calculator *Calculator
}
type Calculator struct{}
func (c *Calculator) Add(a, b int) int { return a + b }
func (c *Calculator) Subtract(a, b int) int { return a - b }
func (c *Calculator) Multiply(a, b int) int { return a * b }
func (s *CalculatorSuite) SetupTest() {
s.calculator = &Calculator{}
}
func (s *CalculatorSuite) TestAddition() {
tests := []struct {
name string
a, b int
expected int
}{
{"2+3", 2, 3, 5},
{"0+0", 0, 0, 0},
{"-1+1", -1, 1, 0},
}
for _, tt := range tests {
s.Run(tt.name, func() {
result := s.calculator.Add(tt.a, tt.b)
s.Equal(tt.expected, result)
})
}
}
func (s *CalculatorSuite) TestSubtraction() {
tests := []struct {
name string
a, b int
expected int
}{
{"5-3", 5, 3, 2},
{"0-0", 0, 0, 0},
{"1-2", 1, 2, -1},
}
for _, tt := range tests {
s.Run(tt.name, func() {
result := s.calculator.Subtract(tt.a, tt.b)
s.Equal(tt.expected, result)
})
}
}
func TestCalculatorSuite(t *testing.T) {
suite.Run(t, new(CalculatorSuite))
}
// 复杂示例:使用Suite夹具
type UserServiceSuite struct {
suite.Suite
service *UserService
testUsers []*User
}
type UserService struct {
users map[int]*User
}
func (s *UserService) GetUser(id int) *User {
return s.users[id]
}
type User struct {
ID int
Name string
Age int
}
func (s *UserServiceSuite) SetupTest() {
s.service = &UserService{
users: make(map[int]*User),
}
s.testUsers = []*User{
{ID: 1, Name: "Alice", Age: 25},
{ID: 2, Name: "Bob", Age: 30},
{ID: 3, Name: "Charlie", Age: 35},
}
for _, user := range s.testUsers {
s.service.users[user.ID] = user
}
}
func (s *UserServiceSuite) TestGetUser() {
tests := []struct {
name string
userID int
expected *User
}{
{"get Alice", 1, s.testUsers[0]},
{"get Bob", 2, s.testUsers[1]},
{"get Charlie", 3, s.testUsers[2]},
}
for _, tt := range tests {
s.Run(tt.name, func() {
user := s.service.GetUser(tt.userID)
s.Equal(tt.expected, user)
})
}
}
func TestUserServiceSuite(t *testing.T) {
suite.Run(t, new(UserServiceSuite))
}
---
6.3 测试覆盖率
01.覆盖率统计
a.go test -cover
使用go test -cover运行测试并显示覆盖率百分比。覆盖率表示被测试执行到的代码行数占总代码行数的比例。高覆盖率不等于高质量,但低覆盖率通常意味着测试不充分。覆盖率是测试完整性的一个重要指标。
b.覆盖率报告
使用-coverprofile生成覆盖率文件,用go tool cover -html查看HTML报告。报告以颜色标示哪些代码被覆盖(绿色)、哪些未覆盖(红色)。这帮助识别测试盲点,指导补充测试用例。
c.代码示例
---
// 覆盖率测试示例
package coverage
import (
"testing"
"github.com/stretchr/testify/assert"
)
// 被测函数
func ProcessData(input string) string {
if input == "" {
return "empty"
}
if len(input) < 3 {
return "short"
}
if input == "special" {
return "special case"
}
return "normal: " + input
}
// 测试覆盖所有分支
func TestProcessData_FullCoverage(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"", "empty"},
{"ab", "short"},
{"special", "special case"},
{"normal input", "normal: normal input"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result := ProcessData(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// 运行命令:
// go test -cover
// go test -coverprofile=coverage.out
// go tool cover -html=coverage.out
---
02.提高覆盖率策略
a.识别未覆盖代码
查看覆盖率报告,找出未被测试的代码路径。关注错误处理分支、边界条件、异常场景。优先为核心业务逻辑和复杂逻辑增加测试。不要为了覆盖率而测试,要关注测试的价值。
b.边界值测试
测试函数在边界条件下的行为如空输入、最大值、最小值等。边界往往是bug高发区。表格驱动测试很适合覆盖多种边界情况。确保所有if、switch分支都被至少测试一次。
c.代码示例
---
// 提高覆盖率示例
package improvecoverage
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func ValidateAge(age int) error {
if age < 0 {
return errors.New("age cannot be negative")
}
if age > 150 {
return errors.New("age too large")
}
if age < 18 {
return errors.New("must be adult")
}
return nil
}
// 完整覆盖所有分支
func TestValidateAge(t *testing.T) {
tests := []struct {
name string
age int
expectError bool
errorMsg string
}{
{"negative age", -1, true, "negative"},
{"too old", 200, true, "too large"},
{"minor", 10, true, "adult"},
{"adult", 25, false, ""},
{"boundary min", 18, false, ""},
{"boundary max", 150, false, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateAge(tt.age)
if tt.expectError {
assert.Error(t, err)
if tt.errorMsg != "" {
assert.Contains(t, err.Error(), tt.errorMsg)
}
} else {
assert.NoError(t, err)
}
})
}
}
---
6.4 基准测试集成
01.基准测试基础
a.Benchmark函数
Go的基准测试函数以Benchmark开头,接收*testing.B参数。在循环中执行被测代码b.N次,Go自动调整N直到结果稳定。使用go test -bench运行基准测试。基准测试衡量代码性能,识别瓶颈。
b.与testify结合
testify的断言可以在基准测试的setup阶段使用,但不要在b.N循环内使用断言,会影响性能测量。可以在基准测试后验证结果正确性。基准测试关注性能,单元测试关注正确性,两者互补。
c.代码示例
---
// 基准测试示例
package bench
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Fibonacci(n int) int {
if n <= 1 {
return n
}
return Fibonacci(n-1) + Fibonacci(n-2)
}
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
// 不同输入的基准测试
func BenchmarkFibonacci10(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(10)
}
}
func BenchmarkFibonacci20(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
// 使用testify验证结果
func TestFibonacciCorrectness(t *testing.T) {
assert.Equal(t, 0, Fibonacci(0))
assert.Equal(t, 1, Fibonacci(1))
assert.Equal(t, 55, Fibonacci(10))
}
// 运行:go test -bench=. -benchmem
---
6.5 并发安全测试
01.Data Race检测
a.go test -race
使用-race标志运行测试,启用Go的data race detector。检测并发访问共享变量时的竞态条件。发现race时会输出详细报告包括冲突的goroutine和代码位置。所有并发代码都应该用-race测试。
b.常见竞态场景
多个goroutine同时读写map。多个goroutine修改同一变量。闭包捕获循环变量。使用mutex、channel或atomic操作保护共享状态。testify的断言本身不是并发安全的。
c.代码示例
---
// 并发安全测试
package racefree
import (
"sync"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
)
// ❌ 不安全的计数器
type UnsafeCounter struct {
count int
}
func (c *UnsafeCounter) Inc() {
c.count++
}
// ✅ 安全的计数器
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Inc() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func TestSafeCounter(t *testing.T) {
counter := &SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Inc()
}()
}
wg.Wait()
assert.Equal(t, 1000, counter.Value())
}
// 使用atomic的计数器
type AtomicCounter struct {
count int64
}
func (c *AtomicCounter) Inc() {
atomic.AddInt64(&c.count, 1)
}
func (c *AtomicCounter) Value() int64 {
return atomic.LoadInt64(&c.count)
}
func TestAtomicCounter(t *testing.T) {
counter := &AtomicCounter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Inc()
}()
}
wg.Wait()
assert.Equal(t, int64(1000), counter.Value())
}
// 运行:go test -race
---
6.6 测试辅助函数
01.自定义断言辅助
a.封装复杂断言
将重复的复杂断言封装成辅助函数。辅助函数接收*testing.T和待验证的值。这减少重复代码,使测试更清晰。辅助函数应该调用t.Helper()标记自己为helper,错误时报告正确的行号。
b.领域特定断言
为项目特定的数据类型创建断言函数。例如验证自定义结构体、检查业务规则等。这使测试代码更贴近业务语言,提高可读性。辅助函数可以在多个测试间复用。
c.代码示例
---
// 测试辅助函数
package helpers
import (
"testing"
"github.com/stretchr/testify/assert"
)
type User struct {
ID int
Name string
Email string
Age int
}
// 自定义断言辅助函数
func AssertValidUser(t *testing.T, user *User) {
t.Helper()
assert.NotNil(t, user)
assert.Greater(t, user.ID, 0)
assert.NotEmpty(t, user.Name)
assert.Contains(t, user.Email, "@")
assert.GreaterOrEqual(t, user.Age, 0)
}
func TestUserCreation(t *testing.T) {
user := &User{
ID: 1,
Name: "Alice",
Email: "[email protected]",
Age: 25,
}
AssertValidUser(t, user)
}
// 领域特定断言
func AssertAdultUser(t *testing.T, user *User) {
t.Helper()
AssertValidUser(t, user)
assert.GreaterOrEqual(t, user.Age, 18, "user must be adult")
}
func TestAdultUser(t *testing.T) {
user := &User{ID: 1, Name: "Bob", Email: "[email protected]", Age: 30}
AssertAdultUser(t, user)
}
// 集合验证辅助
func AssertAllUsersValid(t *testing.T, users []*User) {
t.Helper()
assert.NotEmpty(t, users)
for i, user := range users {
assert.NotNil(t, user, "user at index %d should not be nil", i)
AssertValidUser(t, user)
}
}
func TestUserList(t *testing.T) {
users := []*User{
{ID: 1, Name: "Alice", Email: "[email protected]", Age: 25},
{ID: 2, Name: "Bob", Email: "[email protected]", Age: 30},
}
AssertAllUsersValid(t, users)
}
---
6.7 与CI/CD集成
01.持续集成配置
a.GitHub Actions
在.github/workflows创建workflow文件配置CI。运行go test执行测试,使用go test -race检测竞态条件。可以配置多个Go版本矩阵测试。CI确保每次提交都运行测试,及早发现问题。
b.测试报告
使用-json输出JSON格式的测试结果,便于CI系统解析。配置覆盖率上传到Codecov或Coveralls。设置测试失败时阻止合并。CI提供可视化的测试结果和趋势。
c.代码示例
---
# GitHub Actions配置示例
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [1.20, 1.21, 1.22]
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Install dependencies
run: go mod download
- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.out
---
02.测试优化
a.并行测试
使用t.Parallel()标记可并行的测试。CI环境通常有多核CPU,并行测试大幅减少总时间。确保并行测试间完全独立。可以配置GOMAXPROCS控制并行度。
b.测试缓存
Go自动缓存测试结果,未改变的测试不重复运行。使用go clean -testcache清除缓存。CI环境可以缓存go module和build cache加速。
c.代码示例
---
// CI优化示例
package cioptimize
import (
"testing"
"github.com/stretchr/testify/assert"
)
// 快速单元测试 - 总是并行
func TestFastUnit(t *testing.T) {
t.Parallel()
assert.Equal(t, 4, 2+2)
}
// 慢速集成测试 - 使用build tag隔离
// +build integration
func TestSlowIntegration(t *testing.T) {
// 不并行,需要独占资源
assert.True(t, true)
}
// CI命令示例:
// 快速反馈:go test -short ./...
// 完整测试:go test -v ./...
// 集成测试:go test -tags=integration ./...
---
6.8 测试数据生成
01.测试数据生成策略
a.手动构造
对于简单场景,手动创建测试数据最直接。使用字面量或简单的构造函数。适合数据结构简单、用例少的情况。手动数据易于理解和维护。
b.工厂函数
创建工厂函数生成常用的测试数据。工厂可以接收参数定制数据。复用工厂函数减少重复代码。可以为不同场景创建多个工厂变体。
c.代码示例
---
// 测试数据生成
package testdata
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type Product struct {
ID int
Name string
Price float64
Stock int
}
// 简单工厂
func NewTestProduct(id int) *Product {
return &Product{
ID: id,
Name: fmt.Sprintf("Product%d", id),
Price: 99.99,
Stock: 100,
}
}
// 可定制工厂
func NewTestProductWithPrice(id int, price float64) *Product {
p := NewTestProduct(id)
p.Price = price
return p
}
// Builder模式生成
type ProductBuilder struct {
product *Product
}
func NewProductBuilder() *ProductBuilder {
return &ProductBuilder{
product: &Product{
Price: 0,
Stock: 0,
},
}
}
func (b *ProductBuilder) WithID(id int) *ProductBuilder {
b.product.ID = id
return b
}
func (b *ProductBuilder) WithName(name string) *ProductBuilder {
b.product.Name = name
return b
}
func (b *ProductBuilder) WithPrice(price float64) *ProductBuilder {
b.product.Price = price
return b
}
func (b *ProductBuilder) Build() *Product {
return b.product
}
func TestProductCreation(t *testing.T) {
// 使用工厂
p1 := NewTestProduct(1)
assert.Equal(t, 1, p1.ID)
// 使用Builder
p2 := NewProductBuilder().
WithID(2).
WithName("Custom").
WithPrice(49.99).
Build()
assert.Equal(t, "Custom", p2.Name)
assert.Equal(t, 49.99, p2.Price)
}
// 批量生成
func GenerateTestProducts(count int) []*Product {
products := make([]*Product, count)
for i := 0; i < count; i++ {
products[i] = NewTestProduct(i + 1)
}
return products
}
func TestBulkProducts(t *testing.T) {
products := GenerateTestProducts(10)
assert.Len(t, products, 10)
}
---
02.随机数据与属性测试
a.随机测试数据
使用随机数生成测试数据可以发现边界情况。设置固定seed确保测试可重复。随机测试不应替代精心设计的测试用例,而是补充。记录seed值以便复现失败。
b.属性测试思想
属性测试验证代码的通用属性而非具体输入输出。例如测试排序函数的结果总是有序的。Go生态有gopter等属性测试库。testify主要用于传统基于示例的测试。
c.代码示例
---
// 随机测试数据
package random
import (
"math/rand"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func init() {
// 设置固定seed使测试可重复
rand.Seed(42)
}
func GenerateRandomUser() *User {
return &User{
ID: rand.Intn(10000),
Name: fmt.Sprintf("User%d", rand.Intn(1000)),
Age: rand.Intn(80) + 18,
}
}
func TestRandomUsers(t *testing.T) {
for i := 0; i < 100; i++ {
user := GenerateRandomUser()
// 验证不变属性
assert.Greater(t, user.ID, 0)
assert.NotEmpty(t, user.Name)
assert.GreaterOrEqual(t, user.Age, 18)
}
}
// 属性测试示例
func Reverse(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
func TestReverseProperty(t *testing.T) {
// 属性:反转两次得到原字符串
inputs := []string{"hello", "world", "test", "12345", ""}
for _, input := range inputs {
result := Reverse(Reverse(input))
assert.Equal(t, input, result,
"Reverse(Reverse(x)) should equal x")
}
}
---
7 实战应用
7.1 安装与配置
01.安装testify
a.使用go get
执行go get github.com/stretchr/testify安装testify。这会下载testify及其依赖到项目的go.mod文件。testify是纯Go实现,无需额外依赖。安装后即可在测试文件中import使用。
b.版本管理
使用go.mod管理testify版本。执行go get github.com/stretchr/testify@latest获取最新版本。可以指定特定版本如@v1.8.4。使用go mod tidy清理未使用的依赖。定期更新获取bug修复和新特性。
c.代码示例
---
// 安装命令
// $ go get github.com/stretchr/testify
// go.mod示例
module myproject
go 1.21
require (
github.com/stretchr/testify v1.8.4
)
// 基本使用示例
package main
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/stretchr/testify/mock"
)
func TestBasicSetup(t *testing.T) {
// 使用assert
assert.Equal(t, 2, 1+1)
// 使用require
require.NotNil(t, &struct{}{})
}
// 版本更新命令
// $ go get -u github.com/stretchr/testify
// $ go get github.com/stretchr/[email protected]
// $ go mod tidy
---
02.项目结构
a.测试文件组织
测试文件与源文件放在同一目录,命名为xxx_test.go。使用package xxx_test创建黑盒测试,或package xxx创建白盒测试。黑盒测试只能访问导出的API,更接近用户视角。白盒测试可以访问内部实现,便于测试内部逻辑。
b.测试辅助代码
创建testing包或testutil目录存放共享的测试辅助代码。包括Mock定义、测试数据工厂、自定义断言等。避免在生产代码中引用测试辅助代码。使用internal/testing限制测试辅助代码的可见性。
c.代码示例
---
// 项目结构示例
myproject/
├── go.mod
├── go.sum
├── cmd/
│ └── app/
│ ├── main.go
│ └── main_test.go
├── internal/
│ ├── service/
│ │ ├── user.go
│ │ └── user_test.go
│ └── testing/
│ ├── mocks.go
│ └── factories.go
├── pkg/
│ └── api/
│ ├── handler.go
│ └── handler_test.go
└── test/
├── integration/
│ └── api_test.go
└── e2e/
└── workflow_test.go
// 黑盒测试示例 (package xxx_test)
package service_test
import (
"testing"
"myproject/internal/service"
"github.com/stretchr/testify/assert"
)
func TestUserService(t *testing.T) {
svc := service.NewUserService()
// 只能访问导出的方法
assert.NotNil(t, svc)
}
// 白盒测试示例 (package xxx)
package service
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestInternalLogic(t *testing.T) {
// 可以访问未导出的函数和字段
result := internalHelper()
assert.NotEmpty(t, result)
}
func internalHelper() string {
return "internal"
}
---
03.IDE集成
a.VSCode配置
安装Go扩展获得测试支持。可以在测试函数上方点击"run test"或"debug test"。配置settings.json设置测试参数如-v、-race。使用Test Explorer查看和运行所有测试。
b.GoLand配置
GoLand内置testify支持,自动识别测试。右键测试函数选择Run或Debug。可以配置Run Configuration添加测试参数。查看测试覆盖率高亮显示覆盖的代码。
c.代码示例
---
// VSCode settings.json配置
{
"go.testFlags": ["-v", "-race"],
"go.coverOnSave": true,
"go.coverageDecorator": {
"type": "gutter"
}
}
// GoLand Run Configuration
// Run > Edit Configurations > Go Test
// Program arguments: -v -race -cover
// 命令行运行
// $ go test -v ./...
// $ go test -v -race -cover ./...
// $ go test -v -run TestSpecific
---
7.2 单元测试实战
01.业务逻辑测试
a.测试纯函数
纯函数没有副作用,输入确定则输出确定。是最容易测试的代码。使用表格驱动测试覆盖多种输入。纯函数测试快速且可靠,应该占单元测试的大部分。
b.测试有状态对象
对于有内部状态的对象,在SetupTest中初始化干净状态。测试状态转换逻辑,验证操作后状态正确。使用Suite组织相关的状态测试。每个测试应该独立,不依赖其他测试的状态。
c.代码示例
---
// 单元测试实战
package unittest
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
// 纯函数测试
func CalculateDiscount(price float64, percent float64) float64 {
if percent < 0 || percent > 100 {
return price
}
return price * (1 - percent/100)
}
func TestCalculateDiscount(t *testing.T) {
tests := []struct {
name string
price float64
percent float64
expected float64
}{
{"10% off", 100.0, 10, 90.0},
{"50% off", 100.0, 50, 50.0},
{"no discount", 100.0, 0, 100.0},
{"invalid negative", 100.0, -10, 100.0},
{"invalid over 100", 100.0, 150, 100.0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := CalculateDiscount(tt.price, tt.percent)
assert.InDelta(t, tt.expected, result, 0.01)
})
}
}
// 有状态对象测试
type ShoppingCart struct {
items []Item
total float64
}
type Item struct {
Name string
Price float64
Qty int
}
func NewShoppingCart() *ShoppingCart {
return &ShoppingCart{
items: make([]Item, 0),
total: 0,
}
}
func (c *ShoppingCart) AddItem(item Item) {
c.items = append(c.items, item)
c.total += item.Price * float64(item.Qty)
}
func (c *ShoppingCart) RemoveItem(name string) error {
for i, item := range c.items {
if item.Name == name {
c.total -= item.Price * float64(item.Qty)
c.items = append(c.items[:i], c.items[i+1:]...)
return nil
}
}
return errors.New("item not found")
}
func (c *ShoppingCart) GetTotal() float64 {
return c.total
}
type ShoppingCartSuite struct {
suite.Suite
cart *ShoppingCart
}
func (s *ShoppingCartSuite) SetupTest() {
s.cart = NewShoppingCart()
}
func (s *ShoppingCartSuite) TestAddItem() {
item := Item{Name: "Product1", Price: 10.0, Qty: 2}
s.cart.AddItem(item)
s.Len(s.cart.items, 1)
s.Equal(20.0, s.cart.GetTotal())
}
func (s *ShoppingCartSuite) TestAddMultipleItems() {
s.cart.AddItem(Item{Name: "P1", Price: 10.0, Qty: 1})
s.cart.AddItem(Item{Name: "P2", Price: 20.0, Qty: 2})
s.Len(s.cart.items, 2)
s.Equal(50.0, s.cart.GetTotal())
}
func (s *ShoppingCartSuite) TestRemoveItem() {
s.cart.AddItem(Item{Name: "P1", Price: 10.0, Qty: 1})
s.cart.AddItem(Item{Name: "P2", Price: 20.0, Qty: 1})
err := s.cart.RemoveItem("P1")
s.NoError(err)
s.Len(s.cart.items, 1)
s.Equal(20.0, s.cart.GetTotal())
}
func (s *ShoppingCartSuite) TestRemoveNonexistentItem() {
err := s.cart.RemoveItem("nonexistent")
s.Error(err)
}
func TestShoppingCartSuite(t *testing.T) {
suite.Run(t, new(ShoppingCartSuite))
}
---
02.错误处理测试
a.预期错误测试
使用assert.Error验证函数返回错误。使用assert.ErrorIs或assert.ErrorAs验证特定错误类型。测试错误消息是否包含必要信息。错误处理是代码健壮性的关键,必须充分测试。
b.错误场景覆盖
测试各种可能导致错误的输入。包括nil参数、无效参数、资源不可用等。确保错误路径也被测试覆盖。错误测试帮助发现异常场景下的问题。
c.代码示例
---
// 错误处理测试
package errorhandling
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
var (
ErrInvalidInput = errors.New("invalid input")
ErrNotFound = errors.New("not found")
)
func ProcessUser(id int) (*User, error) {
if id <= 0 {
return nil, ErrInvalidInput
}
if id == 999 {
return nil, ErrNotFound
}
return &User{ID: id, Name: "User"}, nil
}
func TestProcessUser_Success(t *testing.T) {
user, err := ProcessUser(1)
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, 1, user.ID)
}
func TestProcessUser_InvalidInput(t *testing.T) {
user, err := ProcessUser(-1)
assert.Error(t, err)
assert.Nil(t, user)
assert.ErrorIs(t, err, ErrInvalidInput)
}
func TestProcessUser_NotFound(t *testing.T) {
user, err := ProcessUser(999)
assert.Error(t, err)
assert.Nil(t, user)
assert.ErrorIs(t, err, ErrNotFound)
}
// 表格驱动错误测试
func TestProcessUser_AllCases(t *testing.T) {
tests := []struct {
name string
id int
expectError bool
expectedErr error
}{
{"valid", 1, false, nil},
{"zero id", 0, true, ErrInvalidInput},
{"negative id", -1, true, ErrInvalidInput},
{"not found", 999, true, ErrNotFound},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user, err := ProcessUser(tt.id)
if tt.expectError {
assert.Error(t, err)
assert.Nil(t, user)
if tt.expectedErr != nil {
assert.ErrorIs(t, err, tt.expectedErr)
}
} else {
assert.NoError(t, err)
assert.NotNil(t, user)
}
})
}
}
---
7.3 集成测试实战
01.数据库集成测试
a.测试数据库设置
使用测试数据库或容器化数据库如docker的MySQL/PostgreSQL。在SetupSuite中建立数据库连接,在TearDownSuite中关闭。在SetupTest中准备测试数据,在TearDownTest中清理。确保测试数据不污染生产环境。
b.事务回滚策略
在测试中使用事务,测试结束后回滚保持数据库干净。每个测试在事务中执行,互不影响。这比清空表更快且更安全。适合大多数数据库集成测试场景。
c.代码示例
---
// 数据库集成测试
package dbintegration
import (
"database/sql"
"testing"
"github.com/stretchr/testify/suite"
)
type DatabaseSuite struct {
suite.Suite
db *sql.DB
}
func (s *DatabaseSuite) SetupSuite() {
// 连接测试数据库
var err error
s.db, err = sql.Open("mysql", "test:test@tcp(localhost:3306)/testdb")
s.Require().NoError(err)
// 创建测试表
_, err = s.db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
email VARCHAR(100)
)
`)
s.Require().NoError(err)
}
func (s *DatabaseSuite) TearDownSuite() {
// 清理并关闭连接
s.db.Exec("DROP TABLE IF EXISTS users")
s.db.Close()
}
func (s *DatabaseSuite) TearDownTest() {
// 每个测试后清空数据
s.db.Exec("DELETE FROM users")
}
func (s *DatabaseSuite) TestInsertUser() {
result, err := s.db.Exec(
"INSERT INTO users (name, email) VALUES (?, ?)",
"Alice", "[email protected]",
)
s.NoError(err)
id, err := result.LastInsertId()
s.NoError(err)
s.Greater(id, int64(0))
}
func (s *DatabaseSuite) TestQueryUser() {
// 插入测试数据
s.db.Exec("INSERT INTO users (name, email) VALUES (?, ?)",
"Bob", "[email protected]")
// 查询
var name, email string
err := s.db.QueryRow("SELECT name, email FROM users WHERE name = ?", "Bob").
Scan(&name, &email)
s.NoError(err)
s.Equal("Bob", name)
s.Equal("[email protected]", email)
}
func TestDatabaseSuite(t *testing.T) {
suite.Run(t, new(DatabaseSuite))
}
---
02.HTTP API集成测试
a.测试HTTP服务
使用httptest.NewServer启动测试服务器。发送真实的HTTP请求并验证响应。测试完整的请求-响应流程包括路由、中间件、handler。比单独测试handler更接近真实场景。
b.端到端API测试
测试完整的API工作流程如用户注册、登录、操作资源。使用真实的HTTP客户端或测试客户端。验证状态码、响应体、响应头。可以测试认证、授权、错误处理等。
c.代码示例
---
// HTTP API集成测试
package apiintegration
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/suite"
)
type APISuite struct {
suite.Suite
server *httptest.Server
client *http.Client
}
func (s *APISuite) SetupSuite() {
// 创建测试服务器
mux := http.NewServeMux()
mux.HandleFunc("/users", s.handleUsers)
mux.HandleFunc("/users/", s.handleUserByID)
s.server = httptest.NewServer(mux)
s.client = &http.Client{}
}
func (s *APISuite) TearDownSuite() {
s.server.Close()
}
func (s *APISuite) handleUsers(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
json.NewEncoder(w).Encode([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
case "POST":
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(User{ID: 3, Name: "New"})
}
}
func (s *APISuite) handleUserByID(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(User{ID: 1, Name: "Alice"})
}
func (s *APISuite) TestGetUsers() {
resp, err := s.client.Get(s.server.URL + "/users")
s.Require().NoError(err)
defer resp.Body.Close()
s.Equal(http.StatusOK, resp.StatusCode)
var users []User
err = json.NewDecoder(resp.Body).Decode(&users)
s.NoError(err)
s.Len(users, 2)
}
func (s *APISuite) TestCreateUser() {
body := strings.NewReader(`{"name":"Charlie"}`)
resp, err := s.client.Post(s.server.URL+"/users",
"application/json", body)
s.Require().NoError(err)
defer resp.Body.Close()
s.Equal(http.StatusCreated, resp.StatusCode)
var user User
err = json.NewDecoder(resp.Body).Decode(&user)
s.NoError(err)
s.Equal("New", user.Name)
}
func TestAPISuite(t *testing.T) {
suite.Run(t, new(APISuite))
}
---
7.4 Mock实战案例
01.Mock外部依赖
a.Mock数据库
创建数据库接口的Mock实现,控制查询返回的数据。可以测试成功和失败场景,无需真实数据库。Mock使测试快速且可重复。验证数据库方法被正确调用。
b.Mock HTTP客户端
Mock HTTP客户端避免真实网络请求。可以模拟各种响应如成功、超时、错误。测试客户端代码如何处理不同响应。Mock使测试不依赖外部服务。
c.代码示例
---
// Mock实战案例
package mockpractice
import (
"errors"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
// 数据库接口
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
Delete(id int) error
}
// Mock实现
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) FindByID(id int) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func (m *MockUserRepository) Save(user *User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockUserRepository) Delete(id int) error {
args := m.Called(id)
return args.Error(0)
}
// 业务服务
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
func (s *UserService) UpdateUserName(id int, name string) error {
user, err := s.repo.FindByID(id)
if err != nil {
return err
}
user.Name = name
return s.repo.Save(user)
}
// 测试Suite
type UserServiceSuite struct {
suite.Suite
mockRepo *MockUserRepository
service *UserService
}
func (s *UserServiceSuite) SetupTest() {
s.mockRepo = new(MockUserRepository)
s.service = NewUserService(s.mockRepo)
}
func (s *UserServiceSuite) TestGetUser_Success() {
expectedUser := &User{ID: 1, Name: "Alice"}
s.mockRepo.On("FindByID", 1).Return(expectedUser, nil)
user, err := s.service.GetUser(1)
s.NoError(err)
s.Equal(expectedUser, user)
s.mockRepo.AssertExpectations(s.T())
}
func (s *UserServiceSuite) TestGetUser_NotFound() {
s.mockRepo.On("FindByID", 999).
Return(nil, errors.New("not found"))
user, err := s.service.GetUser(999)
s.Error(err)
s.Nil(user)
s.mockRepo.AssertExpectations(s.T())
}
func (s *UserServiceSuite) TestUpdateUserName() {
existingUser := &User{ID: 1, Name: "Old"}
s.mockRepo.On("FindByID", 1).Return(existingUser, nil)
s.mockRepo.On("Save", mock.MatchedBy(func(u *User) bool {
return u.ID == 1 && u.Name == "New"
})).Return(nil)
err := s.service.UpdateUserName(1, "New")
s.NoError(err)
s.mockRepo.AssertExpectations(s.T())
}
func TestUserServiceSuite(t *testing.T) {
suite.Run(t, new(UserServiceSuite))
}
// HTTP客户端Mock
type HTTPClient interface {
Get(url string) ([]byte, error)
Post(url string, data []byte) ([]byte, error)
}
type MockHTTPClient struct {
mock.Mock
}
func (m *MockHTTPClient) Get(url string) ([]byte, error) {
args := m.Called(url)
return args.Get(0).([]byte), args.Error(1)
}
func (m *MockHTTPClient) Post(url string, data []byte) ([]byte, error) {
args := m.Called(url, data)
return args.Get(0).([]byte), args.Error(1)
}
type APIService struct {
client HTTPClient
}
func (s *APIService) FetchData(endpoint string) ([]byte, error) {
return s.client.Get("https://api.example.com/" + endpoint)
}
func TestAPIService(t *testing.T) {
mockClient := new(MockHTTPClient)
service := &APIService{client: mockClient}
expectedData := []byte(`{"status":"ok"}`)
mockClient.On("Get", "https://api.example.com/data").
Return(expectedData, nil)
data, err := service.FetchData("data")
assert := assert.New(t)
assert.NoError(err)
assert.Equal(expectedData, data)
mockClient.AssertExpectations(t)
}
---
7.5 测试重构技巧
01.消除重复代码
a.提取辅助函数
将重复的测试逻辑提取为辅助函数。辅助函数可以在同一测试文件或共享测试包中定义。使用t.Helper()标记辅助函数,错误报告更准确。减少重复提高测试可维护性。
b.使用表格驱动
用表格驱动测试替代多个相似的测试函数。定义测试用例数组,用循环执行。添加新用例只需添加数据。表格驱动使测试更简洁清晰。
c.代码示例
---
// 测试重构示例
package refactor
import (
"testing"
"github.com/stretchr/testify/assert"
)
// ❌ 重构前:重复代码
func TestValidateEmail_Valid(t *testing.T) {
email := "[email protected]"
result := ValidateEmail(email)
assert.True(t, result)
}
func TestValidateEmail_Invalid1(t *testing.T) {
email := "invalid"
result := ValidateEmail(email)
assert.False(t, result)
}
func TestValidateEmail_Invalid2(t *testing.T) {
email := ""
result := ValidateEmail(email)
assert.False(t, result)
}
// ✅ 重构后:表格驱动
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
expected bool
}{
{"valid", "[email protected]", true},
{"invalid no @", "invalid", false},
{"empty", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ValidateEmail(tt.email)
assert.Equal(t, tt.expected, result)
})
}
}
func ValidateEmail(email string) bool {
return len(email) > 3
}
// ❌ 重构前:重复的断言
func TestUser_Original(t *testing.T) {
user := &User{ID: 1, Name: "Alice", Email: "[email protected]"}
assert.NotNil(t, user)
assert.Greater(t, user.ID, 0)
assert.NotEmpty(t, user.Name)
assert.Contains(t, user.Email, "@")
}
// ✅ 重构后:辅助函数
func assertValidUser(t *testing.T, user *User) {
t.Helper()
assert.NotNil(t, user)
assert.Greater(t, user.ID, 0)
assert.NotEmpty(t, user.Name)
assert.Contains(t, user.Email, "@")
}
func TestUser_Refactored(t *testing.T) {
user := &User{ID: 1, Name: "Alice", Email: "[email protected]"}
assertValidUser(t, user)
}
func TestMultipleUsers(t *testing.T) {
users := []*User{
{ID: 1, Name: "Alice", Email: "[email protected]"},
{ID: 2, Name: "Bob", Email: "[email protected]"},
}
for _, user := range users {
assertValidUser(t, user)
}
}
---
02.改善测试结构
a.使用Suite组织
将相关测试组织到Suite中,共享setup和teardown。Suite提供更好的测试组织结构。测试方法更清晰,依赖关系更明确。Suite适合复杂的测试场景。
b.合理分组
按功能或场景将测试分组到不同的Suite或子测试。每个Suite或测试方法关注一个明确的方面。避免巨大的测试方法,拆分成多个小测试。良好的组织使测试易于理解和维护。
c.代码示例
---
// 改善测试结构
package structure
import (
"testing"
"github.com/stretchr/testify/suite"
)
// ❌ 重构前:混乱的测试
func TestEverything(t *testing.T) {
// 测试用户创建
// ... 100行代码
// 测试用户更新
// ... 100行代码
// 测试用户删除
// ... 100行代码
}
// ✅ 重构后:使用Suite
type UserCRUDSuite struct {
suite.Suite
service *UserService
}
func (s *UserCRUDSuite) SetupTest() {
s.service = &UserService{}
}
func (s *UserCRUDSuite) TestCreate() {
// 专注于创建逻辑
user := s.service.CreateUser("Alice")
s.NotNil(user)
}
func (s *UserCRUDSuite) TestUpdate() {
// 专注于更新逻辑
s.Run("update name", func() {
// ...
})
s.Run("update email", func() {
// ...
})
}
func (s *UserCRUDSuite) TestDelete() {
// 专注于删除逻辑
err := s.service.DeleteUser(1)
s.NoError(err)
}
func TestUserCRUDSuite(t *testing.T) {
suite.Run(t, new(UserCRUDSuite))
}
type UserService struct{}
func (s *UserService) CreateUser(name string) *User {
return &User{Name: name}
}
func (s *UserService) DeleteUser(id int) error {
return nil
}
---
7.6 性能测试
01.基准测试编写
a.基准测试函数
基准测试函数以Benchmark开头,接收*testing.B。在b.N循环中执行被测代码。Go会自动调整N使测试运行足够长时间得到稳定结果。使用b.ResetTimer()排除setup时间。
b.性能分析
使用go test -bench运行基准测试。添加-benchmem显示内存分配统计。使用-cpuprofile生成CPU profile文件。结合pprof工具深入分析性能瓶颈。
c.代码示例
---
// 性能测试示例
package performance
import (
"testing"
"github.com/stretchr/testify/assert"
)
func StringConcat(strs []string) string {
result := ""
for _, s := range strs {
result += s
}
return result
}
func StringConcatBuilder(strs []string) string {
var builder strings.Builder
for _, s := range strs {
builder.WriteString(s)
}
return builder.String()
}
// 基准测试
func BenchmarkStringConcat(b *testing.B) {
strs := []string{"hello", "world", "test"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
StringConcat(strs)
}
}
func BenchmarkStringConcatBuilder(b *testing.B) {
strs := []string{"hello", "world", "test"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
StringConcatBuilder(strs)
}
}
// 不同输入大小的基准测试
func BenchmarkStringConcat_Small(b *testing.B) {
strs := []string{"a", "b", "c"}
for i := 0; i < b.N; i++ {
StringConcat(strs)
}
}
func BenchmarkStringConcat_Large(b *testing.B) {
strs := make([]string, 100)
for i := range strs {
strs[i] = "test"
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
StringConcat(strs)
}
}
// 运行命令:
// go test -bench=. -benchmem
// go test -bench=StringConcat -benchtime=5s
// go test -bench=. -cpuprofile=cpu.prof
// 正确性验证(配合基准测试)
func TestStringConcatCorrectness(t *testing.T) {
strs := []string{"hello", "world"}
result := StringConcat(strs)
assert.Equal(t, "helloworld", result)
}
---
02.性能对比测试
a.对比不同实现
为同一功能的不同实现编写基准测试。对比它们的性能差异。帮助选择最优实现。记录性能基线,防止性能退化。
b.性能回归检测
定期运行基准测试,对比历史数据。显著的性能变化需要调查。可以在CI中运行基准测试。使用benchstat等工具对比结果。
c.代码示例
---
// 性能对比测试
package comparison
import (
"testing"
)
// 实现1:使用map
func ContainsMap(items []int, target int) bool {
m := make(map[int]bool)
for _, item := range items {
m[item] = true
}
return m[target]
}
// 实现2:线性查找
func ContainsLinear(items []int, target int) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
func BenchmarkContainsMap(b *testing.B) {
items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
for i := 0; i < b.N; i++ {
ContainsMap(items, 5)
}
}
func BenchmarkContainsLinear(b *testing.B) {
items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
for i := 0; i < b.N; i++ {
ContainsLinear(items, 5)
}
}
// 大数据集对比
func BenchmarkContainsMap_Large(b *testing.B) {
items := make([]int, 10000)
for i := range items {
items[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ContainsMap(items, 5000)
}
}
func BenchmarkContainsLinear_Large(b *testing.B) {
items := make([]int, 10000)
for i := range items {
items[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ContainsLinear(items, 5000)
}
}
---
7.7 常见问题
01.断言失败定位
a.使用子测试
使用t.Run创建子测试,每个子测试有独立的名称。测试失败时会显示完整的测试路径。便于快速定位失败的具体用例。子测试使输出更有组织性。
b.添加有意义的消息
在断言中添加描述性消息。消息应该说明测试的意图和失败的原因。帮助快速理解失败的上下文。特别是在表格驱动测试中很有用。
c.代码示例
---
// 断言失败定位
package debugging
import (
"testing"
"github.com/stretchr/testify/assert"
)
// ❌ 难以定位的测试
func TestPoorLocation(t *testing.T) {
assert.Equal(t, 5, Add(2, 3))
assert.Equal(t, 10, Add(5, 5))
assert.Equal(t, 0, Add(0, 0))
// 如果第二个失败,不容易看出是哪个
}
// ✅ 易于定位:使用子测试
func TestGoodLocation(t *testing.T) {
t.Run("2+3=5", func(t *testing.T) {
assert.Equal(t, 5, Add(2, 3))
})
t.Run("5+5=10", func(t *testing.T) {
assert.Equal(t, 10, Add(5, 5))
})
t.Run("0+0=0", func(t *testing.T) {
assert.Equal(t, 0, Add(0, 0))
})
}
// ✅ 添加有意义的消息
func TestWithMessages(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -2, -3, -5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
assert.Equal(t, tt.expected, result,
"Add(%d, %d) should equal %d but got %d",
tt.a, tt.b, tt.expected, result)
})
}
}
func Add(a, b int) int {
return a + b
}
---
02.Mock调试
a.验证Mock调用
使用AssertExpectations确保Mock被正确调用。使用AssertCalled验证特定调用。使用AssertNumberOfCalls验证调用次数。Mock未按预期调用时会有清晰的错误消息。
b.Mock行为问题
确保On设置的参数匹配实际调用的参数。使用mock.Anything作为占位符。检查Return的返回值类型是否正确。使用Run方法打印调试信息。
c.代码示例
---
// Mock调试
package mockdebug
import (
"fmt"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)
type Service interface {
Process(data string) (string, error)
}
type MockService struct {
mock.Mock
}
func (m *MockService) Process(data string) (string, error) {
args := m.Called(data)
return args.String(0), args.Error(1)
}
// ❌ 常见错误:参数不匹配
func TestMockError(t *testing.T) {
mockSvc := new(MockService)
// 设置期望:"test"
mockSvc.On("Process", "test").Return("result", nil)
// 但实际调用:"TEST"(不匹配)
// result, _ := mockSvc.Process("TEST") // 会失败
// ✅ 正确:参数匹配
result, err := mockSvc.Process("test")
assert.NoError(t, err)
assert.Equal(t, "result", result)
mockSvc.AssertExpectations(t)
}
// ✅ 使用Anything匹配任意参数
func TestMockAnything(t *testing.T) {
mockSvc := new(MockService)
mockSvc.On("Process", mock.Anything).Return("result", nil)
// 任何参数都匹配
mockSvc.Process("test1")
mockSvc.Process("test2")
mockSvc.AssertExpectations(t)
}
// ✅ 调试:使用Run打印信息
func TestMockDebug(t *testing.T) {
mockSvc := new(MockService)
mockSvc.On("Process", mock.Anything).
Run(func(args mock.Arguments) {
fmt.Printf("Process called with: %v\n", args.Get(0))
}).
Return("result", nil)
mockSvc.Process("debug test")
mockSvc.AssertExpectations(t)
}
---
03.测试超时
a.设置测试超时
使用testing.Short()跳过慢速测试。在测试中使用context.WithTimeout控制操作超时。CI环境可以设置全局超时。避免测试无限hang住。
b.处理慢速测试
识别慢速测试并优化。考虑使用Mock替代真实操作。将慢速集成测试与快速单元测试分离。使用build tags标记慢速测试。
c.代码示例
---
// 测试超时处理
package timeout
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// 跳过慢速测试
func TestSlowOperation(t *testing.T) {
if testing.Short() {
t.Skip("skipping slow test in short mode")
}
// 慢速操作
time.Sleep(1 * time.Second)
}
// 使用超时控制
func TestWithTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
done := make(chan bool)
go func() {
// 模拟操作
time.Sleep(50 * time.Millisecond)
done <- true
}()
select {
case <-done:
assert.True(t, true)
case <-ctx.Done():
t.Fatal("operation timed out")
}
}
// 运行命令:
// go test -short (跳过慢速测试)
// go test -timeout 30s (设置全局超时)
---
7.8 调试技巧
01.使用t.Log输出
a.日志记录
使用t.Log或t.Logf在测试中输出调试信息。日志只在测试失败或使用-v标志时显示。帮助理解测试执行过程。记录中间状态和变量值。
b.条件日志
使用t.Failed()检查测试是否已失败。在失败时输出额外的调试信息。避免在成功时输出大量日志。保持测试输出简洁。
c.代码示例
---
// 测试调试技巧
package debug
import (
"testing"
"github.com/stretchr/testify/assert"
)
func ComplexOperation(input int) int {
// 复杂的多步骤操作
step1 := input * 2
step2 := step1 + 10
step3 := step2 / 2
return step3
}
func TestComplexOperation(t *testing.T) {
input := 5
expected := 10
// 记录输入
t.Logf("Testing with input: %d", input)
result := ComplexOperation(input)
// 记录结果
t.Logf("Got result: %d", result)
if !assert.Equal(t, expected, result) {
// 失败时记录更多信息
t.Logf("Expected %d but got %d", expected, result)
}
}
// 详细的调试日志
func TestWithDetailedLogging(t *testing.T) {
data := []int{1, 2, 3, 4, 5}
t.Log("Processing data:", data)
sum := 0
for i, v := range data {
sum += v
t.Logf("Step %d: added %d, sum is now %d", i, v, sum)
}
assert.Equal(t, 15, sum)
if t.Failed() {
t.Log("Test failed, final sum:", sum)
}
}
// 运行:go test -v (显示所有日志)
---
02.断点调试
a.IDE调试器
在IDE中设置断点,以调试模式运行测试。逐步执行代码,检查变量值。VSCode和GoLand都支持测试调试。比日志更直观地理解代码执行。
b.Delve调试器
使用dlv test命令行调试器。在代码中插入断点。检查goroutine状态。调试并发问题时特别有用。
c.代码示例
---
// 调试器使用示例
package debugger
import (
"testing"
"github.com/stretchr/testify/assert"
)
func ProcessData(data []int) []int {
result := make([]int, 0)
for _, v := range data {
// 在这里设置断点
if v > 0 {
result = append(result, v*2)
}
}
return result
}
func TestProcessData(t *testing.T) {
input := []int{-1, 2, -3, 4, 5}
// 在这里设置断点,检查input
result := ProcessData(input)
// 在这里设置断点,检查result
expected := []int{4, 8, 10}
assert.Equal(t, expected, result)
}
// VSCode调试:
// 1. 点击行号左侧设置断点
// 2. 点击测试函数上方的"debug test"
// Delve命令行调试:
// $ dlv test
// (dlv) break ProcessData
// (dlv) continue
// (dlv) print v
// (dlv) next
---
03.表格驱动测试调试
a.隔离失败用例
当表格测试中某个用例失败时,使用-run标志只运行该用例。修改测试临时注释其他用例。使用t.Run的名称过滤。快速迭代修复问题。
b.添加调试用例
在表格中添加临时调试用例。使用极简输入定位问题。逐步接近实际失败的情况。找到根因后移除调试用例。
c.代码示例
---
// 表格驱动测试调试
package tabledebug
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, assert.AnError
}
return a / b, nil
}
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
expectError bool
}{
{"normal", 10, 2, 5, false},
{"zero divisor", 10, 0, 0, true},
{"negative", -10, 2, -5, false},
// 临时调试用例
{"debug case", 7, 2, 3, false}, // 期望3但实际是3.5
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 添加调试日志
t.Logf("Testing: %d / %d", tt.a, tt.b)
result, err := Divide(tt.a, tt.b)
// 详细日志
t.Logf("Result: %d, Error: %v", result, err)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}
// 只运行特定用例:
// go test -v -run TestDivide/normal
// go test -v -run TestDivide/debug
// 调试技巧:
// 1. 添加t.Log输出中间值
// 2. 使用断点检查变量
// 3. 简化测试用例到最小复现
// 4. 逐步添加复杂度直到找到问题
---
04.并发测试调试
a.Race检测
始终使用-race运行并发测试。race detector会报告数据竞争的详细信息。修复所有报告的竞态条件。race检测是发现并发bug的第一道防线。
b.减少并发度
调试时减少goroutine数量。从2-3个goroutine开始。逐步增加找到问题阈值。使用channel或日志同步输出。
c.代码示例
---
// 并发测试调试
package concurrentdebug
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
// ❌ 有bug的并发代码
type UnsafeCounter struct {
count int
}
func (c *UnsafeCounter) Inc() {
c.count++ // data race
}
func (c *UnsafeCounter) Value() int {
return c.count // data race
}
func TestUnsafeCounter(t *testing.T) {
counter := &UnsafeCounter{}
// 调试:先用少量goroutine
var wg sync.WaitGroup
for i := 0; i < 10; i++ { // 从10个开始而不是1000个
wg.Add(1)
go func(n int) {
defer wg.Done()
t.Logf("Goroutine %d incrementing", n)
counter.Inc()
}(i)
}
wg.Wait()
result := counter.Value()
t.Logf("Final value: %d", result)
assert.Equal(t, 10, result)
}
// 运行:go test -race -v
// 会检测到data race并显示详细位置
// ✅ 修复后的版本
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Inc() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func TestSafeCounter(t *testing.T) {
counter := &SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Inc()
}()
}
wg.Wait()
assert.Equal(t, 100, counter.Value())
}
// 调试步骤:
// 1. go test -race 检测竞态
// 2. 减少goroutine数量定位问题
// 3. 添加日志追踪执行顺序
// 4. 使用sync.Mutex等同步原语修复
// 5. 再次运行-race验证修复
---