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

栏目: 后端 · 发布时间: 5年前

内容简介:在本文中我将教你如何用 Go 语言与 Elasticsearch 做单元测试。并且,此方法也适用于几乎所有语言调用外部 RESTful API.假设你有一个日志服务,能够获取获取某个应用,最近 n 条日志。例如下面代码中的

Elastic client 是一款很不错的针对 Go 语言的 Elasticsearch 客户端,在 Working With Elasticsearch 一文中,我用它举例解释了如何对文档建立索引并搜索文档。你如果希望代码能正常执行,不会被重构或者修改所影响,那么你必须要有一个能覆盖所有代码的测试用例。

在本文中我将教你如何用 Go 语言与 Elasticsearch 做单元测试。并且,此方法也适用于几乎所有语言调用外部 RESTful API.

服务调用 Elasticsearch

假设你有一个日志服务,能够获取获取某个应用,最近 n 条日志。例如下面代码中的 GetLog 方法!我提供的是我们生产环境已经再用的代码,方便你了解实际的应用场景。

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 查询结果。

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 获取

如果你不清楚 w.Write([]byte(resp)) 中的 res 为什么需要被转换成 []byte 的话 , 可以看一下这篇文章: How To Correctly Serialize JSON String In Golang

关于测试的一些注意点

尽管本文主要介绍的是如何通过 Go 语言编写外部调用的测试代码,但不得不说集成测试更佳。集成测试是基于整个系统的各个组件共同运行,测试的结果更接近于生产环境,能够提供更高的质量保障。

但是,集成测试一般来说更难实现,而且需要花费更多的时间。因此很少有人编写集成测试的代码。

最后

当测试 Go 客户端访问外服 API 的代码的时候,最好的方式就是 mock 外部服务,如果外服服务是通过结构体实现的时候,可以直接 mock 外部 API,返回一些预制的数据方便我们完成真实情况的测试。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

运营其实很简单:互联网运营进阶之道

运营其实很简单:互联网运营进阶之道

郑文博 / 人民邮电出版社 / 2018-2 / 49.80元

为了帮助从事运营或即将从事运营的广大读者更好、更快地了解运营、学习运营、入职运营,本书详细阐述运营对于用户、企业的帮助,同时以单个理论点 单个实战案例的方式详细分析了社群运营、活动运营、新媒体运营、内容运营、渠道运营、精细化运营、场景化运营、用户化运营、商业化运营等模块及运营工作、渠道整合、社群知识、渠道优化、SOP流程等细节,力求让读者在求职路上快速上手,在迷茫途中快速定位。 《运营其实很简单 ......一起来看看 《运营其实很简单:互联网运营进阶之道》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器