如何通过接口优化Go语言main函数中构造函数的Mock实现?
如何用接口优雅Mock返回接口的构造函数?
我目前的构造函数会返回一个接口,借此可以Mock其方法,但测试时需要在每个用例里替换全局构造函数变量,写法不够优雅。想知道能不能通过接口来Mock这个构造函数(用mockery生成Mock)。
现有代码结构:
// main.go var createSSMClient = ssm.CreateClient // 全局构造函数变量 func main() { _, _, _ = getParams(context.Background()) } func getParams(ctx context.Context) (string, string, error) { ssmClient, err := createSSMClient(ctx) if err != nil { return "", "", err } param1, param2 := ssmClient.GetParams() // ... 业务逻辑 }
// constructor.go func CreateClient(ctx context.Context) (SSMInterface, error) { // ... 实际客户端初始化逻辑 } type SSMInterface interface { GetParams() (string, string) }
当前测试写法:
// main_test.go func TestGetParams(t *testing.T) { tests := []struct{ name string // ... 测试用例参数 }{ // ... 测试用例 } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { createSSMClient = func(ctx context.Context) (ssm.SSMInterface, error) { // 每个用例都要写的Mock构造逻辑 return &MockSSMClient{}, nil } // ... 执行测试断言 }) } }
可以通过定义构造函数的接口来实现更优雅的Mock,结合mockery生成Mock实例,避免每个测试用例重复编写构造逻辑。
步骤1:定义构造函数接口
先为构造函数本身定义一个接口,描述它的签名:
// ssm/ssm.go type SSMClientCreator interface { Create(ctx context.Context) (SSMInterface, error) } // 让原构造函数适配这个接口 type DefaultSSMCreator struct{} func (d *DefaultSSMCreator) Create(ctx context.Context) (SSMInterface, error) { return CreateClient(ctx) // 调用原有的构造函数逻辑 }
步骤2:替换全局变量为接口类型
把原来的全局createSSMClient变量换成上面定义的接口类型:
// main.go var ssmCreator ssm.SSMClientCreator = &ssm.DefaultSSMCreator{} func getParams(ctx context.Context) (string, string, error) { ssmClient, err := ssmCreator.Create(ctx) // 通过接口调用构造 if err != nil { return "", "", err } param1, param2 := ssmClient.GetParams() // ... 业务逻辑 }
步骤3:用mockery生成构造函数的Mock
执行mockery命令生成SSMClientCreator接口的Mock:
mockery --name=SSMClientCreator --output=ssm/mocks --outpkg=mocks
生成的Mock会包含Create方法的Mock实现,你可以在测试中控制它的返回值。
步骤4:优雅编写测试
测试时直接注入Mock实例,无需每个用例重复定义匿名函数:
// main_test.go import ( "your-project/ssm/mocks" "github.com/stretchr/testify/mock" ) func TestGetParams(t *testing.T) { tests := []struct{ name string mockClient ssm.SSMInterface expectErr bool // ... 其他断言参数 }{ { name: "success_get_params", mockClient: func() *mocks.SSMInterface { m := &mocks.SSMInterface{} m.On("GetParams").Return("param1_val", "param2_val") return m }(), expectErr: false, }, // ... 其他测试用例 } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // 创建构造函数的Mock mockCreator := &mocks.SSMClientCreator{} mockCreator.On("Create", mock.Anything).Return(tt.mockClient, nil) // 替换全局的构造器 ssmCreator = mockCreator // 执行测试 p1, p2, err := getParams(context.Background()) // 断言结果 if tt.expectErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.Equal(t, "param1_val", p1) assert.Equal(t, "param2_val", p2) mockCreator.AssertExpectations(t) tt.mockClient.AssertExpectations(t) } }) } }
额外优化:避免全局变量依赖
如果想彻底消除全局变量的测试依赖,可以把ssmCreator作为参数传入getParams函数:
func getParams(ctx context.Context, creator ssm.SSMClientCreator) (string, string, error) { ssmClient, err := creator.Create(ctx) if err != nil { return "", "", err } // ... 业务逻辑 } // main函数中传入默认构造器 func main() { _, _, _ = getParams(context.Background(), &ssm.DefaultSSMCreator{}) }
这种写法更符合依赖注入原则,测试时直接传Mock即可,连全局变量替换都省了。
内容的提问来源于stack exchange,提问作者Kepes
相关产品推荐
相关产品推荐

