Contents

使用httptest 实现golang ES Mock

sqlmoc

在日常开发中,为了保证代码健壮性,和避免手残写错ORM语句,我们难免会遇到mock 测试,在使用MySQL 或者pg 的时候,我们可以使用开源的drive和mock 工具进行快速mock 比如 go-sqlmock,但是如果想对ES 生成对DDL语句做校验的时候,有时候显得没有sqlmock 这么方便,于是就简单的写了一个es 的mock。

思路

我连接ES 使用的是这个第三方包 github.com/olivere/elastic/v7。ES查询实际上就是调用HTTP请求,那么问题就迎刃而解了,我们只需要做一个HTTP mock 就可以解决我们的问题了。

重写相关方法

好在golang 内置的包已经提供了测试 http的包 httptest,接着我们只需要做亿点点改造就可以成为我们的es mock。

首先定义一个struct

type MockHttpClient struct {
	RequestBody  []byte  // except genator ddl
	ResponseBody []byte  // except response
	URL          string  // connect url
}

因为要实现一个http 的clinet,查看文档,控制请求及响应的方法是client Transport 方法,这个方法的数据类型是RoundTripper这个interface,我们只需要实现 RoundTrip这个方法,就可以控制http 的请求及响应了。当然我这里只是简单的实现, 真正的mock比这复杂的多。

func (c *MockHttpClient) RoundTrip(req *http.Request) (*http.Response, error) {
	recorder := httptest.NewRecorder()
	if c.ResponseBody == nil || c.RequestBody == nil {
		return nil, errors.New("ResponseBody or RequestBody can't be null")
	}
	recorder.Header().Set("Content-Type", "application/json")
	reqBody, err := ioutil.ReadAll(req.Body)
	if err != nil {
		return nil, err
	}
	reqReg := regexp.MustCompile(string(c.RequestBody))
	regRes := reqReg.Find(reqBody)
	if string(regRes) == "" {
		return nil, errors.Errorf(`Except query string: "%s" But get "%s"`, reqBody, c.RequestBody)
	}
	if _, err := recorder.Write(c.ResponseBody); err != nil {
		return nil, err
	}
	return recorder.Result(), nil
}

生成ES client

http client 的问题已经解决了,接下来就是用这个假的http client 生成一个es 的client

func (c *MockHttpClient) MockElasticSearchClient() (*elastic.Client, error) {
	client, err := elastic.NewClient(
		elastic.SetURL(c.URL),
		elastic.SetSniff(false),
		elastic.SetHealthcheck(false),
		elastic.SetHttpClient(&http.Client{Transport: c}), elastic.SetTraceLog(logger.NewLogger("ESMock")))

	return client, err
}

再加亿点点细节

新增一个 初始化mock 的方法

func NewMock() (httpMock *MockHttpClient) {
	testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
	defer testServer.Close()
	var mock = &MockHttpClient{
		URL: testServer.URL,
	}
	return mock
}

为 mock client 提供一个 设置request body 和response body 的方法

func (c *MockHttpClient) ShouldRequestQuery(queryString string) *MockHttpClient {
	c.RequestBody = []byte(queryString)
	return c
}
func (c *MockHttpClient) ShouldResponse(Response string) *MockHttpClient {
	c.ResponseBody = []byte(Response)
	return c
}

最后的调用

func TestRepository_AlarmCount(t *testing.T) {
	mock := esMock.NewMock()
	// 设置预期的值
	mock.ShouldRequestQuery(exceptQuery) // exceptQuery 为预期产生的DDL Query
	mock.ShouldResponse(exceptResp) //exceptResp 为预期的响应
	client, err := MockElasticSearchClient()
	// 我们项目把数据请求都封再repository,也可以直接掉client 的search 等方法
	repo, err := esRepository.NewAttackPhaseRepository(client)
	assert.NoError(t, err)
	actualRes, err := repo.AlarmCount(ctx, &esRepository.AlarmFilter{})
	assert.NoError(t, err)
	assert.Equal(t, len(actualRes), 4)
	// 没封装查询方法可以直接写成这样 client.Search("index_name")...
}