使用Elastic进行Golang的模拟测试

huangapple go评论82阅读模式
英文:

Golang Mocking with Elastic

问题

我已经用中文翻译了你的内容:

我用Go语言构建了一个快速简单的API,用于查询ElasticSearch。现在我知道可以做到,我想通过添加测试来正确实现。我已经将一些代码抽象出来,以便进行单元测试,但是在模拟elastic库方面遇到了一些问题,因此我认为最好尝试模拟一个简单的情况。

import (
    "encoding/json"
    "github.com/olivere/elastic"
    "net/http"
)
...
func CheckBucketExists(name string, client *elastic.Client) bool {
    exists, err := client.IndexExists(name).Do()
    if err != nil {
        panic(err)
    }
    return exists
}

现在是测试部分...

import (
    "fmt"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "testing"
)

type MockClient struct {
    mock.Mock
}

func (m *MockClient) IndexExists(name string) (bool, error) {
    args := m.Mock.Called()
    fmt.Println("This is a thing")
    return args.Bool(0), args.Error(1)
}

func TestMockBucketExists(t *testing.T) {
    m := MockClient{}
    m.On("IndexExists", "thisuri").Return(true)

    r := CheckBucketExists("thisuri", &m)
    assert := assert.New(t)
    assert.True(r, true)
}

然后我得到了以下错误:cannot use m (type MockClient) as type *elastic.Client in argument to CheckBucketExists

我猜这可能是我对elastic.client类型使用的一些基本问题,但是我还是太菜了。

英文:

I've built a quick and easy API in Go that queries ElasticSearch. Now that I know it can be done, I want to do it correctly by adding tests. I've abstracted some of my code so that it can be unit-testable, but I've been having some issues mocking the elastic library, and as such I figured it would be best if I tried a simple case to mock just that.

import (
    "encoding/json"
    "github.com/olivere/elastic"
    "net/http"
)
...
func CheckBucketExists(name string, client *elastic.Client) bool {
    exists, err := client.IndexExists(name).Do()
    if err != nil {
        panic(err)
    }
    return exists
}

And now the test...

  import (
      "fmt"
      "github.com/stretchr/testify/assert"
      "github.com/stretchr/testify/mock"
      "testing"
  )

type MockClient struct {
      mock.Mock
  }

  func (m *MockClient) IndexExists(name string) (bool, error) {
      args := m.Mock.Called()
      fmt.Println("This is a thing")
      return args.Bool(0), args.Error(1)
  }

  func TestMockBucketExists(t *testing.T) {
      m := MockClient{}
      m.On("IndexExists", "thisuri").Return(true)

>>    r := CheckBucketExists("thisuri", m)
      assert := assert.New(t)
      assert.True(r, true)
  }

To which I'm yielded with the following error: cannot use m (type MockClient) as type *elastic.Client in argument to CheckBucketExists.

I'm assuming this is something fundamental with my use of the elastic.client type, but I'm still too much of a noob.

答案1

得分: 5

这是一个旧问题,但我也找不到解决方案。
不幸的是,这个库是使用结构体实现的,这使得模拟它变得非常困难,所以我找到的选项有:

(1)在自己的接口上包装所有的elastic.SearchResult方法,并“代理”调用,这样你最终会得到这样的东西:

type ObjectsearchESClient interface {
    // ... 所有方法...
    Do(context.Context) (*elastic.SearchResult, error)
}

// NewObjectsearchESClient 返回 ObjectsearchESClient 的新实现
func NewObjectsearchESClient(cluster *config.ESCluster) (ObjectsearchESClient, error) {
    esClient, err := newESClient(cluster)
    if err != nil {
        return nil, err
    }

    newClient := objectsearchESClient{
        Client: esClient,
    }
    return &newClient, nil
}

// ... 所有方法...
func (oc *objectsearchESClient) Do(ctx context.Context) (*elastic.SearchResult, error) {
    return oc.searchService.Do(ctx)
}

然后像处理应用程序的其他模块一样模拟这个接口和响应。

(2)另一个选项是在这篇博文中指出的,即使用httptest.Server模拟来自 REST 调用的响应。

为此,我模拟了处理程序,即模拟“HTTP 调用”的响应:

func mockHandler() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        resp := `{
            "took": 73,
            "timed_out": false,
            ... json ...
            "hits": [...]
            ... json ...,
            "aggregations": { ... }
        }`

        w.Write([]byte(resp))
    }
}

然后创建一个虚拟的elastic.Client结构:

func mockClient(url string) (*elastic.Client, error) {
    client, err := elastic.NewSimpleClient(elastic.SetURL(url))
    if err != nil {
        return nil, err
    }
    return client, nil
}

在这种情况下,我有一个构建我的elastic.SearchService并返回它的库,所以我使用 HTTP:

...
ts := httptest.NewServer(mockHandler())
defer ts.Close()
esClient, err := mockClient(ts.URL)
ss := elastic.NewSearchService(esClient)
mockLibESClient := es_mock.NewMockSearcherClient(mockCtrl)
mockLibESClient.EXPECT().GetEmployeeSearchServices(ctx).Return(ss, nil)

其中mockLibESClient是我提到的库,我们桩了mockLibESClient.GetEmployeeSearchServices方法,使其返回具有预期有效负载的SearchService

注意:为了创建模拟的mockLibESClient,我使用了 https://github.com/golang/mock

我发现这样做很复杂,但是在我看来,“包装”elastic.Client更费力。

问题:我尝试使用https://github.com/vburenin/ifacemaker创建一个接口,然后使用https://github.com/golang/mock模拟该接口,并且在使用时遇到了兼容性错误。我对 Go 不是很了解,所以可能需要更好地理解类型转换才能解决这个问题。如果你们中的任何人知道如何使用它,请告诉我。

英文:

This is an old question, but couldn't find the solution either.
Unfortunately, this library is implemented using a struct, that makes mocking it not trivial at all, so the options I found are:

(1) Wrap all the elastic.SearchResult Methods on an interface on your own and "proxy" the call, so you end up with something like:

type ObjectsearchESClient interface {
	// ... all methods... 
	Do(context.Context) (*elastic.SearchResult, error)
}


// NewObjectsearchESClient returns a new implementation of ObjectsearchESClient
func NewObjectsearchESClient(cluster *config.ESCluster) (ObjectsearchESClient, error) {
	esClient, err := newESClient(cluster)
	if err != nil {
		return nil, err
	}

	newClient := objectsearchESClient{
		Client: esClient,
	}
	return &newClient, nil
}
// ... all methods... 
func (oc *objectsearchESClient) Do(ctx context.Context) (*elastic.SearchResult, error) {
	return oc.searchService.Do(ctx)
}

And then mock this interface and responses as you would with other modules of your app.

(2) Another option is like pointed in this blog post that is mock the response from the Rest calls using httptest.Server

for this, I mocked the handler, that consist of mocking the response from the "HTTP call"

func mockHandler () http.HandlerFunc{
	return func(w http.ResponseWriter, r *http.Request) {
		resp := `{
			"took": 73,
			"timed_out": false,
			... json ... 
			"hits": [... ]
			...json ... ,
			"aggregations": { ... }
		}`

		w.Write([]byte(resp))
	}
}

Then you create a dummy elastic.Client struct

func mockClient(url string) (*elastic.Client, error) {
	client, err := elastic.NewSimpleClient(elastic.SetURL(url))
	if err != nil {
		return nil, err
	}
	return client, nil
}

In this case, I've a library that builds my elastic.SearchService and returns it, so I use the HTTP like:

	... 
    ts := httptest.NewServer(mockHandler())
	defer ts.Close()
	esClient, err := mockClient(ts.URL)
	ss := elastic.NewSearchService(esClient)
	mockLibESClient := es_mock.NewMockSearcherClient(mockCtrl)
	mockLibESClient.EXPECT().GetEmployeeSearchServices(ctx).Return(ss, nil)

where mockLibESClient is the library I mentioned, and we stub the mockLibESClient.GetEmployeeSearchServices method making it return the SearchService with that will return the expected payload.

Note: for creating the mock mockLibESClient I used https://github.com/golang/mock

I found this to be convoluted, but "Wrapping" the elastic.Client was in my point of view more work.

Question: I tried to mock it by using https://github.com/vburenin/ifacemaker to create an interface, and then mock that interface with https://github.com/golang/mock and kind of use it, but I kept getting compatibility errors when trying to return an interface instead of a struct, I'm not a Go expect at all so probably I needed to understand the typecasting a little better to be able to solve it like that. So if any of you know how to do it with that please let me know.

答案2

得分: 1

elasticsearch的Go客户端Github仓库(https://github.com/elastic/go-elasticsearch)包含一个官方示例(https://github.com/elastic/go-elasticsearch/blob/main/_examples/xkcdsearch/store_test.go),展示了如何模拟elasticsearch客户端。基本上,它涉及使用配置调用NewClient来存根化HTTP传输:

client, err := elasticsearch.NewClient(elasticsearch.Config{
	Transport: &mocktrans,
})
英文:

The elasticsearch go client Github repo contains an official example of how to mock the elasticsearch client. It basically involves calling NewClient with a configuration which stubs the HTTP transport:

client, err := elasticsearch.NewClient(elasticsearch.Config{
	Transport: &mocktrans,
})

答案3

得分: 0

我发现主要有三种方法可以创建一个模拟/虚拟的ES客户端。我的回答不包括与真实Elasticsearch集群进行集成测试。

  1. 您可以按照这篇文章的步骤,使用httptest.Server来模拟Rest调用的响应,最终创建一个虚拟的elastic.Client结构。

  2. 正如在这个链接中由包的作者提到的,您可以通过“指定一个具有两个实现的接口:一个使用真实的ES集群,另一个使用在测试中使用的回调函数”。以下是一个示例供您参考:

type Searcher interface {
	Search(context.Context, SearchRequest) (*SearchResponse, error)
}

// ESSearcher将与真实的ES集群一起使用。
type ESSearcher struct {
	client *elastic.Client
}

func (s *ESSearcher) Search(ctx context.Context, req SearchRequest) (*SearchResponse, error) {
	// 使用s.client对真实的ES集群进行搜索操作
}

// MockedSearcher可以在测试中使用。
type MockedSearcher struct {
	OnSearch func(context.Context, SearchRequest) (*SearchResponse, error)
}

func (s *ESSearcher) Search(ctx context.Context, req SearchRequest) (*SearchResponse, error) {
	return s.OnSearch(ctx, req)
}
  1. 最后,正如在同一个链接中作者提到的,您可以“在测试中运行一个真实的Elasticsearch集群”。一个特别好的方法可能是在测试期间使用类似于github.com/ory/dockertest的工具启动ES集群。以下是一个示例供您参考:
package search

import (
	"context"
	"fmt"
	"log"
	"os"
	"testing"

	"github.com/olivere/elastic/v7"
	"github.com/ory/dockertest/v3"
	"github.com/ory/dockertest/v3/docker"
)

// client将在TestMain中初始化
var client *elastic.Client

func TestMain(m *testing.M) {
	pool, err := dockertest.NewPool("")
	if err != nil {
		log.Fatalf("无法创建新的资源池:%v", err)
	}

	options := &dockertest.RunOptions{
		Repository: "docker.elastic.co/elasticsearch/elasticsearch-oss",
		Tag:        "7.8.0",
		PortBindings: map[docker.Port][]docker.PortBinding{
			"9200": {{HostPort: "9200"}},
		},
		Env: []string{
			"cluster.name=elasticsearch",
			"bootstrap.memory_lock=true",
			"discovery.type=single-node",
			"network.publish_host=127.0.0.1",
			"logger.org.elasticsearch=warn",
			"ES_JAVA_OPTS=-Xms1g -Xmx1g",
		},
	}
	resource, err := pool.RunWithOptions(options)
	if err != nil {
		log.Fatalf("无法运行ES:%v", err)
	}
	endpoint := fmt.Sprintf("http://127.0.0.1:%s", resource.GetPort("9200/tcp"))

	if err := pool.Retry(func() error {
		var err error
		client, err = elastic.NewClient(
			elastic.SetURL(endpoint),
			elastic.SetSniff(false),
			elastic.SetHealthcheck(false),
		)
		if err != nil {
			return err
		}
		_, _, err = client.Ping(endpoint).Do(context.Background())
		if err != nil {
			return err
		}
		return nil
	}); err != nil {
		log.Fatalf("无法连接到ES:%v", err)
	}

	code := m.Run()

	if err := pool.Purge(resource); err != nil {
		log.Fatalf("无法停止ES:%v", err)
	}

	os.Exit(code)
}

func TestAgainstRealCluster(t *testing.T) {
	// 在这里可以使用"client"变量

	// 示例代码:
	exists, err := client.IndexExists("cities-test").Do(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	if !exists {
		t.Fatal("预期找到ES索引")
	}
}

以上是您要翻译的内容。

英文:

There are primarily three ways I discovered to create a Mock/Dumy ES client. My response does not include integration tests against a real Elasticsearch cluster.

  1. You can follow this article so as to mock the response from the Rest calls using httptest.Server, to eventually create a dummy elastic.Client struct

  2. As mentioned by the package author in this link, you can work on "specifying an interface that has two implementations: One that uses a real ES cluster, and one that uses callbacks used in testing. Here's an example to get you started:"


type Searcher interface {
Search(context.Context, SearchRequest) (*SearchResponse, error)
}
// ESSearcher will be used with a real ES cluster.
type ESSearcher struct {
client *elastic.Client
}
func (s *ESSearcher) Search(ctx context.Context, req SearchRequest) (*SearchResponse, error) {
// Use s.client to run against real ES cluster and perform a search
}
// MockedSearcher can be used in testing.
type MockedSearcher struct {
OnSearch func(context.Context, SearchRequest) (*SearchResponse, error)
}
func (s *ESSearcher) Search(ctx context.Context, req SearchRequest) (*SearchResponse, error) {
return s.OnSearch(ctx, req)
}
  1. Finally, as mentioned by the author in the same link you can "run a real Elasticsearch cluster while testing. One particular nice way might be to start the ES cluster during testing with something like github.com/ory/dockertest. Here's an example to get you started:"
package search
import (
"context"
"fmt"
"log"
"os"
"testing"
"github.com/olivere/elastic/v7"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
)
// client will be initialize in TestMain
var client *elastic.Client
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("unable to create new pool: %v", err)
}
options := &dockertest.RunOptions{
Repository: "docker.elastic.co/elasticsearch/elasticsearch-oss",
Tag:        "7.8.0",
PortBindings: map[docker.Port][]docker.PortBinding{
"9200": {{HostPort: "9200"}},
},
Env: []string{
"cluster.name=elasticsearch",
"bootstrap.memory_lock=true",
"discovery.type=single-node",
"network.publish_host=127.0.0.1",
"logger.org.elasticsearch=warn",
"ES_JAVA_OPTS=-Xms1g -Xmx1g",
},
}
resource, err := pool.RunWithOptions(options)
if err != nil {
log.Fatalf("unable to ES: %v", err)
}
endpoint := fmt.Sprintf("http://127.0.0.1:%s", resource.GetPort("9200/tcp"))
if err := pool.Retry(func() error {
var err error
client, err = elastic.NewClient(
elastic.SetURL(endpoint),
elastic.SetSniff(false),
elastic.SetHealthcheck(false),
)
if err != nil {
return err
}
_, _, err = client.Ping(endpoint).Do(context.Background())
if err != nil {
return err
}
return nil
}); err != nil {
log.Fatalf("unable to connect to ES: %v", err)
}
code := m.Run()
if err := pool.Purge(resource); err != nil {
log.Fatalf("unable to stop ES: %v", err)
}
os.Exit(code)
}
func TestAgainstRealCluster(t *testing.T) {
// You can use "client" variable here
// Example code:
exists, err := client.IndexExists("cities-test").Do(context.Background())
if err != nil {
t.Fatal(err)
}
if !exists {
t.Fatal("expected to find ES index")
}
}

答案4

得分: -1

这是要翻译的内容:

该行代码

func CheckBucketExists(name string, client *elastic.Client) bool {

表示CheckBucketExists函数期望一个*elastic.Client

以下代码行:

m := MockClient{}
m.On("IndexExists", "thisuri").Return(true) 
r := CheckBucketExists("thisuri", m)

将一个MockClient传递给了CheckBucketExists函数。

这导致了类型冲突。

也许你需要在测试文件中导入github.com/olivere/elastic并进行以下操作:

m := &elastic.Client{}

而不是

m := MockClient{}

但我不能百分之百确定你想要做什么。

英文:

The line

func CheckBucketExists(name string, client *elastic.Client) bool {

states that CheckBucketExists expects a *elastic.Client.

The lines:

m := MockClient{}
m.On("IndexExists", "thisuri").Return(true) 
r := CheckBucketExists("thisuri", m)

pass a MockClient to the CheckBucketExists function.

This is causing a type conflict.

Perhaps you need to import github.com/olivere/elastic into your test file and do:

m := &elastic.Client{}

instead of

m := MockClient{}

But I'm not 100% sure what you're trying to do.

huangapple
  • 本文由 发表于 2015年2月11日 20:27:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/28454337.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定