对 Golang 代码调用 Elasticsearch 进行单元测试

JYSDeveloper · · 1467 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

[Elastic client](https://github.com/olivere/elastic) 是一款很不错的针对 Go 语言的 Elasticsearch 客户端,在[Working With Elasticsearch](http://goinbigdata.com/working-with-elasticsearch-in-go/) 一文中,我用它举例解释了如何对文档建立索引并搜索文档。你如果希望代码能正常执行,不会被重构或者修改所影响,那么你必须要有一个能覆盖所有代码的测试用例。 在本文中我将教你如何用 Go 语言与 Elasticsearch 做单元测试。并且,此方法也适用于几乎所有语言调用外部 RESTful API. ## 服务调用 Elasticsearch 假设你有一个日志服务,能够获取获取某个应用,最近 n 条日志。例如下面代码中的 `GetLog` 方法!我提供的是我们生产环境已经再用的代码,方便你了解实际的应用场景。 ```go package logging import ( "gopkg.in/olivere/elastic.v3" "reflect" ) type Service interface { GetLog(app string, lines int) ([]string, error) } func NewService(url string) (Service, error) { client, err := elastic.NewSimpleClient(elastic.SetURL(url)) if err != nil { return nil, err } return &service{elasticClient: client}, nil } type service struct { elasticClient *elastic.Client } type Log struct { Message string `json:"message"` } // GetLog returns limited tail of log sorted by time in ascending order func (s *service) GetLog(app string, limit int) ([]string, error) { termQuery := elastic.NewTermQuery("app", app) res, err := s.elasticClient.Search("_all"). Query(termQuery). Sort("@timestamp", false). Size(limit). Do() if err != nil { return nil, err } msgNum := len(res.Hits.Hits) if msgNum == 0 { return []string{}, nil } messages := make([]string, msgNum, msgNum) var l Log for i, item := range res.Each(reflect.TypeOf(l)) { l := item.(Log) messages[i] = l.Message } // Reversing messages for i := 0; i < msgNum/2; i++ { messages[i], messages[msgNum-(i+1)] = messages[msgNum-(i+1)], messages[i] } return messages, nil } ``` 日志是首先通过 Elasticsearch 倒序获取过来的,转换一下格式之后,在将结果返回给调用端。 ## 对服务进行单元测试 一般来讲,我们可以通过 mock 客户端的方式来进行单元测试。不过, `elastic.Client` 是用结构体实现的,所以想要 mock 它的话很麻烦。 更深层次的解决方式应该是 mock Elasticsearch 的 API,这种方式就简单多了。一个解决办法是通过 `httptest.Server` 访问一个预制好的服务接口,这里只返回一些预定义好的 Elasticsearch 查询结果。 ```go package logging import ( "github.com/stretchr/testify/assert" "gopkg.in/olivere/elastic.v3" "net/http" "net/http/httptest" "testing" ) func TestLog(t *testing.T) { handler := http.NotFound ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler(w, r) })) defer ts.Close() handler = func(w http.ResponseWriter, r *http.Request) { resp := `{ "took" : 122, "timed_out" : false, "_shards" : { "total" : 6, "successful" : 5, "failed" : 1, "failures" : [ { "shard" : 0, "index" : ".kibana", "node" : "jucBX9QkQIini9dLG9tZIw", "reason" : { "type" : "search_parse_exception", "reason" : "No mapping found for [offset] in order to sort on" } } ] }, "hits" : { "total" : 10, "max_score" : null, "hits" : [ { "_index" : "logstash-2016.07.25", "_type" : "log", "_id" : "AVYkNv542Gim_t2htKPU", "_score" : null, "_source" : { "message" : "Alice message 10", "@version" : "1", "@timestamp" : "2016-07-25T22:39:55.760Z", "source" : "/Users/yury/logs/alice.log", "offset" : 144, "type" : "log", "input_type" : "log", "count" : 1, "fields" : null, "beat" : { "hostname" : "Yurys-MacBook-Pro.local", "name" : "Yurys-MacBook-Pro.local" }, "host" : "Yurys-MacBook-Pro.local", "tags" : [ "beats_input_codec_plain_applied" ], "app" : "alice" }, "sort" : [ 144 ] }, { "_index" : "logstash-2016.07.25", "_type" : "log", "_id" : "AVYkNv542Gim_t2htKPT", "_score" : null, "_source" : { "message" : "Alice message 9", "@version" : "1", "@timestamp" : "2016-07-25T22:39:55.760Z", "source" : "/Users/yury/logs/alice.log", "offset" : 128, "input_type" : "log", "count" : 1, "beat" : { "hostname" : "Yurys-MacBook-Pro.local", "name" : "Yurys-MacBook-Pro.local" }, "type" : "log", "fields" : null, "host" : "Yurys-MacBook-Pro.local", "tags" : [ "beats_input_codec_plain_applied" ], "app" : "alice" }, "sort" : [ 128 ] }, { "_index" : "logstash-2016.07.25", "_type" : "log", "_id" : "AVYkNv542Gim_t2htKPR", "_score" : null, "_source" : { "message" : "Alice message 8", "@version" : "1", "@timestamp" : "2016-07-25T22:39:55.760Z", "type" : "log", "input_type" : "log", "source" : "/Users/yury/logs/alice.log", "count" : 1, "fields" : null, "beat" : { "hostname" : "Yurys-MacBook-Pro.local", "name" : "Yurys-MacBook-Pro.local" }, "offset" : 112, "host" : "Yurys-MacBook-Pro.local", "tags" : [ "beats_input_codec_plain_applied" ], "app" : "alice" }, "sort" : [ 112 ] } ] } }` w.Write([]byte(resp)) } s, err := MockService(ts.URL) assert.NoError(t, err) expectedMessages := []string{ "Alice message 8", "Alice message 9", "Alice message 10", } actualMessages, err := s.GetLog("app", 3) assert.NoError(t, err) assert.Equal(t, expectedMessages, actualMessages) } func MockService(url string) (Service, error) { client, err := elastic.NewSimpleClient(elastic.SetURL(url)) if err != nil { return nil, err } return &service{elasticClient: client}, nil } ``` 预制的结果可以提前写入到一个文件里面,在代码里读取就可以了。源代码可以访问[GitHub](https://github.com/upitau/goinbigdata/tree/master/examples/elastictest) 获取 > 如果你不清楚 `w.Write([]byte(resp))` 中的 `res` 为什么需要被转换成 []byte 的话 , 可以看一下这篇文章: [How To Correctly Serialize JSON String In Golang](http://goinbigdata.com/how-to-correctly-serialize-json-string-in-golang/)。 ## 关于测试的一些注意点 尽管本文主要介绍的是如何通过 Go 语言编写外部调用的测试代码,但不得不说集成测试更佳。集成测试是基于整个系统的各个组件共同运行,测试的结果更接近于生产环境,能够提供更高的质量保障。 但是,集成测试一般来说更难实现,而且需要花费更多的时间。因此很少有人编写集成测试的代码。 ## 最后 当测试 Go 客户端访问外服 API 的代码的时候,最好的方式就是 mock 外部服务,如果外服服务是通过结构体实现的时候,可以直接 mock 外部 API,返回一些预制的数据方便我们完成真实情况的测试。

via: http://goinbigdata.com/unit-testing-golang-code-calling-elasticsearch/

作者:Yury Pitsishin  译者:JYSDeveloper  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1467 次点击  ∙  1 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
上一篇:gin 总结
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传