如何Mock Go语言的Elasticsearch SDK v8?单元测试遇阻求助
如何Mock Elasticsearch SDK v8进行单元测试?
问题描述
我用Go开发API,基于Elasticsearch SDK v8做交互,单元测试时Mock ES方法遇到了麻烦。现有代码如下:
业务函数
func DocumentExists(id string) (bool, error) { exists, err := elastic.Client.Exists("my_index", "doc_id") if err != nil { return false, err } if exists.StatusCode != 200 { return false, nil } return true, nil }
全局ES Client定义
import elastic "github.com/elastic/go-elasticsearch/v8" var ( Client *elastic.Client )
尝试用testify Mock但没成功,想知道怎么实现Mock,是否必须改Client的获取方式(比如用接口契约),而且我觉得当前场景下契约可能没必要。
解决方案
方式一:不引入接口,直接替换全局Client(满足你“契约非必需”的需求)
因为elastic.Client是全局变量,测试时可以直接替换成Mock实例。不过ES v8 SDK本身没提供可直接Mock的接口,所以得自己封装匹配返回结构的逻辑,结合testify/mock来实现:
- 先定义一个模拟的响应结构体,实现ES SDK的
esapi.Response接口(只需要用到StatusCode,其他方法空实现即可):
import ( "io" "net/http" "strings" "fmt" "github.com/elastic/go-elasticsearch/v8/esapi" ) type MockExistsResponse struct { StatusCode int } // 实现esapi.Response接口的所有方法 func (m *MockExistsResponse) Body() io.ReadCloser { return io.NopCloser(strings.NewReader("")) } func (m *MockExistsResponse) Header() http.Header { return http.Header{} } func (m *MockExistsResponse) IsError() bool { return m.StatusCode >= 400 } func (m *MockExistsResponse) Status() string { return http.StatusText(m.StatusCode) } func (m *MockExistsResponse) String() string { return fmt.Sprintf("status: %d", m.StatusCode) }
- 创建Mock Client结构体,实现
Exists方法:
import ( "github.com/stretchr/testify/mock" "github.com/elastic/go-elasticsearch/v8/esapi" ) type MockESClient struct { mock.Mock } // 匹配原Client的Exists方法签名 func (m *MockESClient) Exists(index, id string, o ...func(*esapi.ExistsRequest)) (*esapi.Response, error) { args := m.Called(index, id, o) return args.Get(0).(*esapi.Response), args.Error(1) }
- 单元测试中替换全局Client并设置预期:
import ( "testing" "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" elastic "github.com/elastic/go-elasticsearch/v8" "github.com/elastic/go-elasticsearch/v8/esapi" ) func TestDocumentExists(t *testing.T) { // 创建Mock实例 mockClient := new(MockESClient) // 保存原Client,测试后恢复 originalClient := elastic.Client elastic.Client = mockClient defer func() { elastic.Client = originalClient }() // 测试场景1:文档存在(返回200) t.Run("document exists", func(t *testing.T) { mockResp := &esapi.Response{StatusCode: 200} mockClient.On("Exists", "my_index", "doc_id", mock.Anything).Return(mockResp, nil) exists, err := DocumentExists("doc_id") assert.NoError(t, err) assert.True(t, exists) mockClient.AssertExpectations(t) }) // 测试场景2:文档不存在(返回404) t.Run("document not exists", func(t *testing.T) { mockResp := &esapi.Response{StatusCode: 404} mockClient.On("Exists", "my_index", "doc_id", mock.Anything).Return(mockResp, nil) exists, err := DocumentExists("doc_id") assert.NoError(t, err) assert.False(t, exists) mockClient.AssertExpectations(t) }) // 测试场景3:请求出错 t.Run("request error", func(t *testing.T) { mockErr := fmt.Errorf("connection failed") mockClient.On("Exists", "my_index", "doc_id", mock.Anything).Return(nil, mockErr) exists, err := DocumentExists("doc_id") assert.Error(t, err) assert.Equal(t, mockErr, err) assert.False(t, exists) mockClient.AssertExpectations(t) }) }
方式二:引入接口(契约),提升可测试性(可选)
如果后续要扩展更多ES操作,引入接口会让测试更简洁。只需要定义包含所需方法的接口,让业务代码依赖接口而非具体的elastic.Client:
- 定义接口并修改全局变量类型:
import "github.com/elastic/go-elasticsearch/v8/esapi" type ESClient interface { Exists(index, id string, o ...func(*esapi.ExistsRequest)) (*esapi.Response, error) } // 全局变量改为接口类型 var Client ESClient
- 业务代码不用改(因为
*elastic.Client天然实现了这个接口),测试时直接Mock接口:
// Mock结构体和之前一致 type MockESClient struct { mock.Mock } func (m *MockESClient) Exists(index, id string, o ...func(*esapi.ExistsRequest)) (*esapi.Response, error) { args := m.Called(index, id, o) return args.Get(0).(*esapi.Response), args.Error(1) } // 测试逻辑和方式一基本相同,替换Client更直接 func TestDocumentExistsWithInterface(t *testing.T) { mockClient := new(MockESClient) originalClient := Client Client = mockClient defer func() { Client = originalClient }() // ... 重复之前的测试场景即可 }
内容的提问来源于stack exchange,提问作者wiliamvj
相关产品推荐
相关产品推荐

