-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathkit.go
359 lines (335 loc) · 9.53 KB
/
kit.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
package mockit
import (
"database/sql"
"database/sql/driver"
"github.com/DATA-DOG/go-sqlmock"
"github.com/alicebob/miniredis"
"github.com/golang/mock/gomock"
"github.com/jarcoal/httpmock"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"net/http"
"reflect"
"testing"
"unsafe"
)
// Mockit mock工具集,集成如下
// 1. http (elasticSearch)
// 2. MYSQL (支持多类sql,目前支持了mysql,如需添加请联系)
// 3. Redis
// 4. interface (GRPC)
// NOTE: 需结合https://github.com/golang/mock使用,生成后需要将实例化返回值改为interface{},如: NewMockXXX(ctrl *gomock.Controller) interface{}
type Mockit interface {
// GetInterfaceClient 获取interface的mockClient,获取后 m.(*MockXX)获得实例
GetInterfaceClient(name string) interface{}
// MysqlExecExpect mysql 增删改使用
MysqlExecExpect(ep ExpectParam, tb testing.TB)
// MysqlQueryExpect mysql 查询使用
MysqlQueryExpect(ep ExpectParam, tb testing.TB)
// InterfaceExpect interface mockgen 生成使用
// NOTE: grpc 推荐mockgen生成后使用此方案作为mock代理
InterfaceExpect(ep ExpectParam, tb testing.TB)
// HttpExpect http client 拦截使用
// NOTE: ES也是此方案
HttpExpect(ep ExpectParam, tb testing.TB, httpStatus ...int)
// BeforeTest NOTE:清洁单元测试环境
BeforeTest()
// AfterTest NOTE:配合BeforeTest
AfterTest()
// SqlDB 获取sql conn
SqlDB() *sql.DB
// InterceptHttpClient 拦截http client
InterceptHttpClient(client *http.Client)
// RedisAddr 获取伪redis server addr
RedisAddr() string
}
type mockit struct {
sqlEx sqlmock.Sqlmock // sql excepted
redisSrv *miniredis.Miniredis // redis server
sqlDB *sql.DB
ormTag string // 实体解析的tag
clientInitFs []MockClientInitFunc
iFaceClients map[string]interface{}
iFaceValue map[string]reflect.Value
}
type MockClientInitFunc func(ctrl *gomock.Controller) interface{}
// NewWithMockTag 创建mock工具包
// @ormTag: mysql 映射的实体tag,如下可使用"mock"作为tag数据库映射,
// NOTE: gorm中可以带primary key;varchar等,请自定义tag使用
// type Kit struct {
// Name string "mock:"name"
// }
// @fs: 使用mockgen生成的interface mock client
// NOTE: 一般默认生成的是 NewMockDemoInterface(ctrl *gomock.Controller) *MockDemoInterface
// TODO: 需要修改成 NewMockDemoInterface(ctrl *gomock.Controller) interface{}后传入,方可拦截代理
func NewWithMockTag(tag string, fs ...MockClientInitFunc) (Mockit, error) {
kit := new(mockit)
kit.iFaceClients = make(map[string]interface{})
kit.iFaceValue = make(map[string]reflect.Value)
kit.clientInitFs = fs
kit.ormTag = tag
sqlDB, mock, err := sqlmock.New()
if err != nil {
return nil, errors.WithStack(err)
}
kit.sqlDB = sqlDB
kit.sqlEx = mock
// redis
kit.redisSrv, err = miniredis.Run()
if err != nil {
return nil, errors.WithStack(err)
}
// 初始化interface mock 并通过内部反射解耦原库使用
t := log.New()
t.SetFormatter(&log.TextFormatter{
DisableColors: true,
FullTimestamp: true,
})
kit.initGoMockInterface(t)
// 启用httpmock拦截
httpmock.Activate()
return kit, nil
}
// New 创建mock工具包
// @fs: 使用mockgen生成的interface mock client
// NOTE: 一般默认生成的是 NewMockDemoInterface(ctrl *gomock.Controller) *MockDemoInterface
// TODO: 需要修改成 NewMockDemoInterface(ctrl *gomock.Controller) interface{}后传入,方可拦截代理
func New(fs ...MockClientInitFunc) (Mockit, error) {
return NewWithMockTag("mock", fs...)
}
type argsKind uint8
const (
notSet argsKind = 0
normal argsKind = 1
byIndex argsKind = 2
)
func (mk *mockit) GetInterfaceClient(name string) interface{} {
if mk == nil {
return nil
}
return mk.iFaceClients[name]
}
func (mk *mockit) MysqlExecExpect(ep ExpectParam, tb testing.TB) {
p := ep.(*expectParam)
if len(p.method) == 0 {
tb.Fatal("there is no SQL statements in the method filed")
}
if len(p.returns) != 2 {
tb.Fatal("returns requires the addition of lastInsertId and rowsAffected, and the type is int64")
}
var num1, num2 int64
for k, v := range p.returns {
var numTmp int64
switch tmp := v.(type) {
case uint:
numTmp = int64(tmp)
case int:
numTmp = int64(tmp)
case int64:
numTmp = int64(tmp)
case int32:
numTmp = int64(tmp)
case uint32:
numTmp = int64(tmp)
case uint64:
numTmp = int64(tmp)
default:
tb.Fatal("the returns type must be int of the func mysqlExecExpect")
}
if k == 0 {
num1 = numTmp
} else {
num2 = numTmp
}
}
var args []driver.Value
for _, v := range p.args {
args = append(args, v)
}
mk.sqlEx.ExpectBegin()
mk.sqlEx.ExpectExec(p.method).WithArgs(args...).WillReturnResult(sqlmock.NewResult(num1, num2))
mk.sqlEx.ExpectCommit()
}
var errorInterface = reflect.TypeOf((*error)(nil)).Elem()
func (mk *mockit) MysqlQueryExpect(ep ExpectParam, tb testing.TB) {
var (
rows *sqlmock.Rows
errExpected bool
err error
)
p := ep.(*expectParam)
if len(p.method) == 0 {
tb.Fatal("there is no SQL statements in the method filed")
}
if len(p.key) > 0 {
rows = sqlmock.NewRows([]string{p.key}).AddRow(p.val)
} else {
if len(p.returns) != 1 {
tb.Fatal("the query must return one result")
}
typ := reflect.TypeOf(p.returns[0])
if typ.Implements(errorInterface) {
errExpected = true
} else {
rows, err = sqlmock.NewRowsFromInterface(p.returns[0], mk.ormTag)
}
}
if err != nil {
tb.Fatalf("new rows failed:%s", err)
}
var args []driver.Value
for _, v := range p.args {
args = append(args, v)
}
if errExpected {
mk.sqlEx.ExpectQuery(p.method).WithArgs(args...).WillReturnError(p.returns[0].(error))
} else {
mk.sqlEx.ExpectQuery(p.method).WithArgs(args...).WillReturnRows(rows)
}
}
func (mk *mockit) InterfaceExpect(ep ExpectParam, tb testing.TB) {
p := ep.(*expectParam)
if len(p.path) == 0 || len(p.method) == 0 {
tb.Fatalf("both path and method are required")
}
cli, exists := mk.iFaceValue[p.path]
if !exists {
tb.Fatalf("interface mock client %s not exists", p.path)
}
method := cli.Elem().MethodByName(p.method)
if !method.IsValid() {
tb.Fatalf("path:%s method:%s not exists", p.path, p.method)
}
numIn := method.Type().NumIn()
inputs := make([]reflect.Value, numIn)
switch p.argsKind {
case normal:
for i, arg := range p.args {
inputs[i] = reflect.ValueOf(arg)
}
case byIndex:
for i := 0; i < numIn; i++ {
if arg, exists := p.idxArgs[i]; exists {
inputs[i] = reflect.ValueOf(gomock.Eq(arg))
} else {
inputs[i] = reflect.ValueOf(gomock.Any())
}
}
default:
for i := 0; i < numIn; i++ {
inputs[i] = reflect.ValueOf(gomock.Any())
}
}
outputs := method.Call(inputs)
if len(outputs) != 1 {
tb.Fatal("method returns not match")
}
methodReturn := outputs[0].MethodByName("Return")
originalMethod, exists := reflect.TypeOf(mk.iFaceClients[p.path]).MethodByName(p.method)
if !exists {
tb.Fatal("mock client is not formatted correctly")
}
var types []reflect.Type
for i := 0; i < originalMethod.Type.NumOut(); i++ {
types = append(types, originalMethod.Type.Out(i))
}
rs := reflect.New(reflect.TypeOf([]interface{}{})).Elem()
for i, v := range p.returns {
val := reflect.ValueOf(v)
if val.IsValid() {
rs = reflect.Append(rs, val)
} else {
rs = reflect.Append(rs, reflect.Zero(types[i]))
}
}
methodReturn.CallSlice([]reflect.Value{rs})
}
func (mk *mockit) HttpExpect(ep ExpectParam, tb testing.TB, httpStatus ...int) {
var (
resp httpmock.Responder
err error
)
p := ep.(*expectParam)
if len(p.path) == 0 || len(p.method) == 0 {
tb.Fatalf("both path and method are required")
}
status := http.StatusOK
if len(httpStatus) > 0 {
status = httpStatus[0]
}
switch p.key {
case HttpResponseFunc:
if p.responseHandler == nil {
tb.Fatal("responseHandler is nil")
}
resp = p.responseHandler
case HttpResponseString:
str, ok := p.val.(string)
if !ok {
tb.Fatal("return val must be string type")
}
resp = httpmock.NewStringResponder(status, str)
case HttpResponseJson:
resp, err = httpmock.NewJsonResponder(status, p.val)
if err != nil {
tb.Fatalf("NewJsonResponder failed:%s", err)
}
default:
if len(p.returns) != 1 {
tb.Fatal("response nil, please set response via any one of WithKeyValReturn/WithHttpResponseHandler/WithReturns")
}
arg := p.returns[0]
switch argTmp := arg.(type) {
case string:
resp = httpmock.NewStringResponder(status, argTmp)
default:
resp, err = httpmock.NewJsonResponder(status, arg)
if err != nil {
tb.Fatalf("NewJsonResponder failed:%s", err)
}
}
}
httpmock.RegisterResponder(
p.method,
p.path,
resp,
)
}
// 初始化interface mock 并通过内部反射解耦原库使用
func (mk *mockit) initGoMockInterface(t gomock.TestReporter) {
ctrl := gomock.NewController(t)
for _, f := range mk.clientInitFs {
cli := f(ctrl)
val := reflect.ValueOf(cli)
typ := val.Elem().Type()
if typ.Kind() != reflect.Struct {
panic("mock client must gen form mockgen")
}
cliName := typ.Name()
mk.iFaceClients[cliName] = cli
expect := val.Elem().FieldByName("recorder")
unsafeExpect := reflect.NewAt(expect.Type(), unsafe.Pointer(expect.UnsafeAddr()))
mk.iFaceValue[cliName] = unsafeExpect
}
}
func (mk *mockit) BeforeTest() {
httpmock.Activate()
}
func (mk *mockit) AfterTest() {
httpmock.DeactivateAndReset()
}
func (mk *mockit) InterceptHttpClient(client *http.Client) {
httpmock.ActivateNonDefault(client)
}
func (mk *mockit) RedisAddr() string {
if mk == nil {
return ""
}
return mk.redisSrv.Addr()
}
func (mk *mockit) SqlDB() *sql.DB {
if mk == nil {
return nil
}
return mk.sqlDB
}