Skip to content

Commit b51e19b

Browse files
committed
feat(fxelasticsearch): add mock elastic search client with http transport-level mocking capabilities
1 parent ba961aa commit b51e19b

File tree

5 files changed

+367
-111
lines changed

5 files changed

+367
-111
lines changed

fxelasticsearch/README.md

Lines changed: 81 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@
22

33
[![ci](https://github.com/ankorstore/yokai-contrib/actions/workflows/fxelasticsearch-ci.yml/badge.svg)](https://github.com/ankorstore/yokai-contrib/actions/workflows/fxelasticsearch-ci.yml)
44
[![go report](https://goreportcard.com/badge/github.com/ankorstore/yokai-contrib/fxelasticsearch)](https://goreportcard.com/report/github.com/ankorstore/yokai-contrib/fxelasticsearch)
5-
[![codecov](https://codecov.io/gh/ankorstore/yokai-contrib/graph/badge.svg?token=ghUBlFsjhR&flag=fxelasticsearch)](https://app.codecov.io/gh/ankorstore/yokai-contrib/tree/main/fxelasticsearch)
5+
[![codecov](https://codecov.io/gh/ankorstore/yokai-contrib/graph/badge.svg?token=ghUBlFsjhR&flag=fxelasticsearch)](https://codecov.io/gh/ankorstore/yokai-contrib/fxelasticsearch)
66
[![Deps](https://img.shields.io/badge/osi-deps-blue)](https://deps.dev/go/github.com%2Fankorstore%2Fyokai-contrib%2Ffxelasticsearch)
77
[![PkgGoDev](https://pkg.go.dev/badge/github.com/ankorstore/yokai-contrib/fxelasticsearch)](https://pkg.go.dev/github.com/ankorstore/yokai-contrib/fxelasticsearch)
88

9-
> [Fx](https://uber-go.github.io/fx/) module for [Elasticsearch](https://www.elastic.co/elasticsearch/).
9+
> [Fx](https://uber-go.github.io/fx/) module for [Elasticsearch](https://github.com/elastic/go-elasticsearch).
1010
1111
<!-- TOC -->
1212
* [Overview](#overview)
1313
* [Installation](#installation)
1414
* [Configuration](#configuration)
15-
* [Usage](#usage)
15+
* [Testing](#testing)
1616
<!-- TOC -->
1717

1818
## Overview
1919

2020
This module provides to your Fx application an [elasticsearch.Client](https://pkg.go.dev/github.com/elastic/go-elasticsearch/v8),
21-
that you can `inject` anywhere to interact with [Elasticsearch](https://www.elastic.co/elasticsearch/).
21+
that you can `inject` anywhere to interact with [Elasticsearch](https://github.com/elastic/go-elasticsearch).
2222

2323
## Installation
2424

@@ -64,33 +64,91 @@ modules:
6464
password: ${ELASTICSEARCH_PASSWORD}
6565
```
6666
67-
## Usage
67+
Notes:
68+
- The `modules.elasticsearch.address` configuration key is mandatory
69+
- The `modules.elasticsearch.username` and `modules.elasticsearch.password` configuration keys are optional
70+
- See [Elasticsearch configuration](https://pkg.go.dev/github.com/elastic/go-elasticsearch/v8#Config) documentation for more details
6871

69-
Inject the Elasticsearch client in your services:
72+
## Testing
73+
74+
In `test` mode, an additional mock Elasticsearch client is provided with HTTP transport-level mocking capabilities.
75+
76+
### Automatic Test Environment Support
77+
78+
When `APP_ENV=test`, the module automatically provides a default mock Elasticsearch client that returns empty successful responses. This allows your application to start and run basic tests without any additional setup.
79+
80+
### Custom Mock Clients
81+
82+
For specific test scenarios, you can create custom mock clients with controlled responses:
7083

7184
```go
72-
package service
85+
package service_test
7386
7487
import (
75-
"context"
76-
"github.com/elastic/go-elasticsearch/v8"
77-
"github.com/elastic/go-elasticsearch/v8/esapi"
88+
"context"
89+
"errors"
90+
"strings"
91+
"testing"
92+
93+
"github.com/ankorstore/yokai-contrib/fxelasticsearch"
94+
"github.com/stretchr/testify/assert"
7895
)
7996
80-
type MyService struct {
81-
es *elasticsearch.Client
97+
func TestMyService_Search(t *testing.T) {
98+
// Define mock response
99+
mockResponse := `{
100+
"took": 5,
101+
"timed_out": false,
102+
"hits": {
103+
"total": {"value": 1},
104+
"hits": [
105+
{
106+
"_source": {"title": "Test Document", "content": "Test content"}
107+
}
108+
]
109+
}
110+
}`
111+
112+
// Create mock Elasticsearch client
113+
esClient, err := fxelasticsearch.NewMockESClientWithSingleResponse(mockResponse, 200)
114+
assert.NoError(t, err)
115+
116+
// Use the mock client in your service
117+
service := NewMyService(esClient)
118+
119+
// Test your service methods that use Elasticsearch
120+
results, err := service.Search(context.Background(), "test-index", "test query")
121+
assert.NoError(t, err)
122+
assert.Len(t, results, 1)
123+
assert.Equal(t, "Test Document", results[0]["title"])
82124
}
83125

84-
func NewMyService(es *elasticsearch.Client) *MyService {
85-
return &MyService{es: es}
86-
}
126+
### Using Injected Mock Client in Tests
127+
128+
You can also use the automatically provided mock client in Fx-based tests:
87129

88-
func (s *MyService) SearchDocuments(ctx context.Context, index string, query string) (*esapi.Response, error) {
89-
// Use the Elasticsearch client to search for documents
90-
return s.es.Search(
91-
s.es.Search.WithContext(ctx),
92-
s.es.Search.WithIndex(index),
93-
s.es.Search.WithBody(strings.NewReader(query)),
94-
)
130+
```go
131+
func TestWithFxInjection(t *testing.T) {
132+
t.Setenv("APP_ENV", "test")
133+
134+
var esClient *elasticsearch.Client
135+
136+
app := fxtest.New(
137+
t,
138+
fxconfig.FxConfigModule,
139+
fxelasticsearch.FxElasticsearchModule,
140+
fx.Populate(&esClient),
141+
)
142+
143+
app.RequireStart()
144+
145+
// The injected client is a mock that returns empty successful responses
146+
res, err := esClient.Search()
147+
assert.NoError(t, err)
148+
assert.Equal(t, 200, res.StatusCode)
149+
150+
app.RequireStop()
95151
}
96-
```
152+
```
153+
154+
See [example](module_test.go).

fxelasticsearch/mock.go

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,105 @@
11
package fxelasticsearch
22

33
import (
4-
"github.com/elastic/go-elasticsearch/v8/esapi"
5-
"github.com/stretchr/testify/mock"
4+
"io"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/elastic/go-elasticsearch/v8"
69
)
710

8-
// ElasticsearchClientInterface defines the methods for the Elasticsearch client.
9-
// Extend this interface as needed.
10-
type ElasticsearchClientInterface interface {
11-
Search(indices []string, body interface{}) (*esapi.Response, error)
11+
// MockResponse represents a single mock HTTP response.
12+
type MockResponse struct {
13+
StatusCode int
14+
ResponseBody string
15+
Error error
16+
}
17+
18+
// MockTransport provides HTTP transport mock for testing Elasticsearch client.
19+
// This allows testing with the real elasticsearch.Client API without interface constraints.
20+
type MockTransport struct {
21+
responses []MockResponse
22+
index int
1223
}
1324

14-
// ElasticsearchClientMock implements ElasticsearchClientInterface.
15-
// Add methods as needed for testing.
16-
type ElasticsearchClientMock struct {
17-
mock.Mock
25+
// NewMockTransport creates a new MockTransport with the given responses.
26+
// Responses are returned in order. If more requests are made than responses provided,
27+
// the last response is repeated.
28+
func NewMockTransport(responses []MockResponse) *MockTransport {
29+
if len(responses) == 0 {
30+
responses = []MockResponse{{StatusCode: 200, ResponseBody: "{}"}}
31+
}
32+
33+
return &MockTransport{
34+
responses: responses,
35+
index: 0,
36+
}
37+
}
38+
39+
// RoundTrip implements the http.RoundTripper interface.
40+
func (t *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
41+
// Get current response (or last one if we've exceeded the list)
42+
responseIndex := t.index
43+
if responseIndex >= len(t.responses) {
44+
responseIndex = len(t.responses) - 1
45+
} else {
46+
t.index++
47+
}
48+
49+
mockResp := t.responses[responseIndex]
50+
51+
if mockResp.Error != nil {
52+
return nil, mockResp.Error
53+
}
54+
55+
// Create HTTP response
56+
response := &http.Response{
57+
StatusCode: mockResp.StatusCode,
58+
Body: io.NopCloser(strings.NewReader(mockResp.ResponseBody)),
59+
Header: make(http.Header),
60+
Request: req,
61+
}
62+
63+
// Add Elasticsearch-specific headers that the client expects
64+
response.Header.Set("Content-Type", "application/json")
65+
response.Header.Set("X-Elastic-Product", "Elasticsearch") // Critical header for client validation
66+
67+
return response, nil
1868
}
1969

20-
func (m *ElasticsearchClientMock) Search(indices []string, body interface{}) (*esapi.Response, error) {
21-
args := m.Called(indices, body)
22-
if args.Get(0) == nil {
23-
return nil, args.Error(1)
70+
// NewMockESClient creates an Elasticsearch client with mocked HTTP transport.
71+
// This allows you to test with the real elasticsearch.Client API while controlling responses.
72+
func NewMockESClient(responses []MockResponse) (*elasticsearch.Client, error) {
73+
transport := NewMockTransport(responses)
74+
75+
cfg := elasticsearch.Config{
76+
Transport: transport,
2477
}
2578

26-
resp, ok := args.Get(0).(*esapi.Response)
27-
if !ok {
28-
return nil, args.Error(1)
79+
return elasticsearch.NewClient(cfg)
80+
}
81+
82+
// NewMockESClientWithSingleResponse creates an Elasticsearch client with a single mock response.
83+
// This is a convenience function for simple test cases.
84+
func NewMockESClientWithSingleResponse(responseBody string, statusCode int) (*elasticsearch.Client, error) {
85+
responses := []MockResponse{
86+
{
87+
StatusCode: statusCode,
88+
ResponseBody: responseBody,
89+
},
2990
}
3091

31-
return resp, args.Error(1)
92+
return NewMockESClient(responses)
3293
}
3394

34-
// Ensure ElasticsearchClientMock implements ElasticsearchClientInterface.
35-
var _ ElasticsearchClientInterface = (*ElasticsearchClientMock)(nil)
95+
// NewMockESClientWithError creates an Elasticsearch client that returns an error on requests.
96+
// This is useful for testing error handling.
97+
func NewMockESClientWithError(err error) (*elasticsearch.Client, error) {
98+
responses := []MockResponse{
99+
{
100+
Error: err,
101+
},
102+
}
103+
104+
return NewMockESClient(responses)
105+
}

0 commit comments

Comments
 (0)