You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/12/11 08:50:19 UTC

[apisix-dashboard] branch master updated: feat: add a unit test for consumer and remove implicit init (#859)

This is an automated email from the ASF dual-hosted git repository.

juzhiyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new f938c0c  feat: add a unit test for consumer and remove implicit init (#859)
f938c0c is described below

commit f938c0cb1c142b03082936e6560c195c8e784fad
Author: Vinci Xu <27...@qq.com>
AuthorDate: Fri Dec 11 16:50:12 2020 +0800

    feat: add a unit test for consumer and remove implicit init (#859)
    
    * feat: add a unit test for consumer and remove implicit init
    
    * fix: add implict init function to compatible integration tests
    
    * chore: add other cosumer unit test and refacotor some code
    
    * fix: remove intergration instead of unit test
    
    * fix: add EOL for file
    
    * chore: use sub test to run table test
    
    * chore: test desc
    
    * chore: test desc
    
    Co-authored-by: Wen Ming <mo...@gmail.com>
---
 api/conf/conf.go                               |   4 +
 api/filter/logging_test.go                     |  34 +-
 api/internal/core/store/store.go               |   2 +-
 api/internal/core/store/store_mock.go          |  64 +++
 api/internal/handler/consumer/consumer.go      |  52 +--
 api/internal/handler/consumer/consumer_test.go | 597 ++++++++++++++++---------
 api/log/zap.go                                 |   5 +-
 7 files changed, 483 insertions(+), 275 deletions(-)

diff --git a/api/conf/conf.go b/api/conf/conf.go
index b34c339..6ea9af0 100644
--- a/api/conf/conf.go
+++ b/api/conf/conf.go
@@ -101,7 +101,11 @@ type Config struct {
 	Authentication Authentication
 }
 
+// TODO: it is just for integration tests, we should call "InitLog" explicitly when remove all handler's integration tests
 func init() {
+	InitConf()
+}
+func InitConf() {
 	//go test
 	if workDir := os.Getenv("APISIX_API_WORKDIR"); workDir != "" {
 		WorkDir = workDir
diff --git a/api/filter/logging_test.go b/api/filter/logging_test.go
index 087d8b0..688befa 100644
--- a/api/filter/logging_test.go
+++ b/api/filter/logging_test.go
@@ -17,30 +17,30 @@
 package filter
 
 import (
-        "net/http"
-        "net/http/httptest"
-        "testing"
+	"net/http"
+	"net/http/httptest"
+	"testing"
 
-        "github.com/gin-gonic/gin"
-        "github.com/stretchr/testify/assert"
+	"github.com/gin-gonic/gin"
+	"github.com/stretchr/testify/assert"
 
-        "github.com/apisix/manager-api/log"
+	"github.com/apisix/manager-api/log"
 )
 
 func performRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
-        req := httptest.NewRequest(method, path, nil)
-        w := httptest.NewRecorder()
-        r.ServeHTTP(w, req)
-        return w
+	req := httptest.NewRequest(method, path, nil)
+	w := httptest.NewRecorder()
+	r.ServeHTTP(w, req)
+	return w
 }
 
 func TestRequestLogHandler(t *testing.T) {
-        r := gin.New()
-        logger := log.GetLogger(log.AccessLog)
-        r.Use(RequestLogHandler(logger))
-        r.GET("/", func(c *gin.Context) {
-        })
+	r := gin.New()
+	logger := log.GetLogger(log.AccessLog)
+	r.Use(RequestLogHandler(logger))
+	r.GET("/", func(c *gin.Context) {
+	})
 
-        w := performRequest(r, "GET", "/")
-        assert.Equal(t, 200, w.Code)
+	w := performRequest(r, "GET", "/")
+	assert.Equal(t, 200, w.Code)
 }
diff --git a/api/internal/core/store/store.go b/api/internal/core/store/store.go
index d9c7e36..ee3fa50 100644
--- a/api/internal/core/store/store.go
+++ b/api/internal/core/store/store.go
@@ -38,7 +38,7 @@ type Interface interface {
 	Get(key string) (interface{}, error)
 	List(input ListInput) (*ListOutput, error)
 	Create(ctx context.Context, obj interface{}) error
-	Update(ctx context.Context, obj interface{}, createOnFail bool) error
+	Update(ctx context.Context, obj interface{}, createIfNotExist bool) error
 	BatchDelete(ctx context.Context, keys []string) error
 }
 
diff --git a/api/internal/core/store/store_mock.go b/api/internal/core/store/store_mock.go
new file mode 100644
index 0000000..258f083
--- /dev/null
+++ b/api/internal/core/store/store_mock.go
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package store
+
+import (
+	"context"
+	"github.com/stretchr/testify/mock"
+)
+
+type MockInterface struct {
+	mock.Mock
+}
+
+func (m *MockInterface) Get(key string) (interface{}, error) {
+	ret := m.Mock.Called(key)
+	return ret.Get(0), ret.Error(1)
+}
+
+func (m *MockInterface) List(input ListInput) (*ListOutput, error) {
+	ret := m.Called(input)
+
+	var (
+		r0 *ListOutput
+		r1 error
+	)
+
+	if rf, ok := ret.Get(0).(func(ListInput) *ListOutput); ok {
+		r0 = rf(input)
+	} else {
+		r0 = ret.Get(0).(*ListOutput)
+	}
+	r1 = ret.Error(1)
+
+	return r0, r1
+}
+
+func (m *MockInterface) Create(ctx context.Context, obj interface{}) error {
+	ret := m.Mock.Called(ctx, obj)
+	return ret.Error(0)
+}
+
+func (m *MockInterface) Update(ctx context.Context, obj interface{}, createOnFail bool) error {
+	ret := m.Mock.Called(ctx, obj, createOnFail)
+	return ret.Error(0)
+}
+
+func (m *MockInterface) BatchDelete(ctx context.Context, keys []string) error {
+	ret := m.Mock.Called(ctx, keys)
+	return ret.Error(0)
+}
diff --git a/api/internal/handler/consumer/consumer.go b/api/internal/handler/consumer/consumer.go
index a5854c5..31b7af3 100644
--- a/api/internal/handler/consumer/consumer.go
+++ b/api/internal/handler/consumer/consumer.go
@@ -17,21 +17,17 @@
 package consumer
 
 import (
-	"fmt"
-	"net/http"
 	"reflect"
 	"strings"
 
 	"github.com/gin-gonic/gin"
 	"github.com/shiningrush/droplet"
-	"github.com/shiningrush/droplet/data"
 	"github.com/shiningrush/droplet/wrapper"
 	wgin "github.com/shiningrush/droplet/wrapper/gin"
 
 	"github.com/apisix/manager-api/internal/core/entity"
 	"github.com/apisix/manager-api/internal/core/store"
 	"github.com/apisix/manager-api/internal/handler"
-	"github.com/apisix/manager-api/internal/utils"
 )
 
 type Handler struct {
@@ -56,7 +52,7 @@ func (h *Handler) ApplyRoute(r *gin.Engine) {
 	r.PUT("/apisix/admin/consumers", wgin.Wraps(h.Update,
 		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
 	r.DELETE("/apisix/admin/consumers/:usernames", wgin.Wraps(h.BatchDelete,
-		wrapper.InputType(reflect.TypeOf(BatchDelete{}))))
+		wrapper.InputType(reflect.TypeOf(BatchDeleteInput{}))))
 }
 
 type GetInput struct {
@@ -134,19 +130,9 @@ func (h *Handler) List(c droplet.Context) (interface{}, error) {
 
 func (h *Handler) Create(c droplet.Context) (interface{}, error) {
 	input := c.Input().(*entity.Consumer)
-	if input.ID != nil && utils.InterfaceToString(input.ID) != input.Username {
-		return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
-			fmt.Errorf("consumer's id and username must be a same value")
-	}
 	input.ID = input.Username
 
-	if _, ok := input.Plugins["jwt-auth"]; ok {
-		jwt := input.Plugins["jwt-auth"].(map[string]interface{})
-		jwt["exp"] = 86400
-
-		input.Plugins["jwt-auth"] = jwt
-	}
-
+	ensurePluginsDefValue(input.Plugins)
 	if err := h.consumerStore.Create(c.Context(), input); err != nil {
 		return handler.SpecCodeResponse(err), err
 	}
@@ -161,42 +147,34 @@ type UpdateInput struct {
 
 func (h *Handler) Update(c droplet.Context) (interface{}, error) {
 	input := c.Input().(*UpdateInput)
-	if input.ID != nil && utils.InterfaceToString(input.ID) != input.Username {
-		return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
-			fmt.Errorf("consumer's id and username must be a same value")
-	}
 	if input.Username != "" {
 		input.Consumer.Username = input.Username
 	}
 	input.Consumer.ID = input.Consumer.Username
-
-	if _, ok := input.Consumer.Plugins["jwt-auth"]; ok {
-		jwt := input.Consumer.Plugins["jwt-auth"].(map[string]interface{})
-		jwt["exp"] = 86400
-
-		input.Consumer.Plugins["jwt-auth"] = jwt
-	}
+	ensurePluginsDefValue(input.Plugins)
 
 	if err := h.consumerStore.Update(c.Context(), &input.Consumer, true); err != nil {
-		//if not exists, create
-		if err.Error() == fmt.Sprintf("key: %s is not found", input.Username) {
-			if err := h.consumerStore.Create(c.Context(), &input.Consumer); err != nil {
-				return handler.SpecCodeResponse(err), err
-			}
-		} else {
-			return handler.SpecCodeResponse(err), err
-		}
+		return handler.SpecCodeResponse(err), err
 	}
 
 	return nil, nil
 }
 
-type BatchDelete struct {
+func ensurePluginsDefValue(plugins map[string]interface{}) {
+	if plugins["jwt-auth"] != nil {
+		jwtAuth, ok := plugins["jwt-auth"].(map[string]interface{})
+		if ok && jwtAuth["exp"] == nil {
+			jwtAuth["exp"] = 86400
+		}
+	}
+}
+
+type BatchDeleteInput struct {
 	UserNames string `auto_read:"usernames,path"`
 }
 
 func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) {
-	input := c.Input().(*BatchDelete)
+	input := c.Input().(*BatchDeleteInput)
 
 	if err := h.consumerStore.BatchDelete(c.Context(), strings.Split(input.UserNames, ",")); err != nil {
 		return handler.SpecCodeResponse(err), err
diff --git a/api/internal/handler/consumer/consumer_test.go b/api/internal/handler/consumer/consumer_test.go
index ba02f28..6a82154 100644
--- a/api/internal/handler/consumer/consumer_test.go
+++ b/api/internal/handler/consumer/consumer_test.go
@@ -18,232 +18,391 @@
 package consumer
 
 import (
-	"encoding/json"
-
-	"testing"
-	"time"
-
-	"github.com/shiningrush/droplet"
-	"github.com/stretchr/testify/assert"
-
-	"github.com/apisix/manager-api/conf"
+	"context"
+	"fmt"
 	"github.com/apisix/manager-api/internal/core/entity"
-	"github.com/apisix/manager-api/internal/core/storage"
 	"github.com/apisix/manager-api/internal/core/store"
+	"github.com/shiningrush/droplet"
+	"github.com/shiningrush/droplet/data"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"net/http"
+	"testing"
 )
 
-func TestConsumer(t *testing.T) {
-	// init
-	err := storage.InitETCDClient(conf.ETCDConfig)
-	assert.Nil(t, err)
-	err = store.InitStores()
-	assert.Nil(t, err)
+func TestHandler_Get(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *GetInput
+		giveRet    interface{}
+		giveErr    error
+		wantErr    error
+		wantGetKey string
+		wantRet    interface{}
+	}{
+		{
+			caseDesc:   "normal",
+			giveInput:  &GetInput{Username: "test"},
+			wantGetKey: "test",
+			giveRet:    "hello",
+			wantRet:    "hello",
+		},
+		{
+			caseDesc:   "store get failed",
+			giveInput:  &GetInput{Username: "failed key"},
+			wantGetKey: "failed key",
+			giveErr:    fmt.Errorf("get failed"),
+			wantErr:    fmt.Errorf("get failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			mStore := &store.MockInterface{}
+			mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				assert.Equal(t, tc.wantGetKey, args.Get(0))
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{consumerStore: mStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Get(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestHandler_List(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		giveInput *ListInput
+		giveData  []*entity.Consumer
+		giveErr   error
+		wantErr   error
+		wantInput store.ListInput
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "list all condition",
+			giveInput: &ListInput{
+				Username: "testUser",
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			giveData: []*entity.Consumer{
+				{Username: "user1"},
+				{Username: "testUser"},
+				{Username: "iam-testUser"},
+				{Username: "testUser-is-me"},
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					&entity.Consumer{Username: "testUser"},
+					&entity.Consumer{Username: "iam-testUser"},
+					&entity.Consumer{Username: "testUser-is-me"},
+				},
+				TotalSize: 3,
+			},
+		},
+		{
+			caseDesc: "store list failed",
+			giveInput: &ListInput{
+				Username: "testUser",
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			giveData: []*entity.Consumer{},
+			giveErr:  fmt.Errorf("list failed"),
+			wantErr:  fmt.Errorf("list failed"),
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			mStore := &store.MockInterface{}
+			mStore.On("List", mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(0).(store.ListInput)
+				assert.Equal(t, tc.wantInput.PageSize, input.PageSize)
+				assert.Equal(t, tc.wantInput.PageNumber, input.PageNumber)
+			}).Return(func(input store.ListInput) *store.ListOutput {
+				var returnData []interface{}
+				for _, c := range tc.giveData {
+					if input.Predicate(c) {
+						returnData = append(returnData, c)
+					}
+				}
+				return &store.ListOutput{
+					Rows:      returnData,
+					TotalSize: len(returnData),
+				}
+			}, tc.giveErr)
 
-	handler := &Handler{
-		consumerStore: store.GetStore(store.HubKeyConsumer),
+			h := Handler{consumerStore: mStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.List(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
 	}
-	assert.NotNil(t, handler)
-
-	//create consumer
-	ctx := droplet.NewContext()
-	consumer := &entity.Consumer{}
-	reqBody := `{
-      "username": "jack",
-      "plugins": {
-          "limit-count": {
-              "count": 2,
-              "time_window": 60,
-              "rejected_code": 503,
-              "key": "remote_addr"
-          }
-      },
-    "desc": "test description"
-  }`
-	err = json.Unmarshal([]byte(reqBody), consumer)
-	assert.Nil(t, err)
-	ctx.SetInput(consumer)
-	_, err = handler.Create(ctx)
-	assert.Nil(t, err)
-
-	//create consumer 2
-	consumer2 := &entity.Consumer{}
-	reqBody = `{
-		"username": "pony",
-		"plugins": {
-		  "limit-count": {
-		      "count": 2,
-		      "time_window": 60,
-		      "rejected_code": 503,
-		      "key": "remote_addr"
-		  }
+}
+
+func TestHandler_Create(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *entity.Consumer
+		giveCtx    context.Context
+		giveErr    error
+		wantErr    error
+		wantInput  *entity.Consumer
+		wantRet    interface{}
+		wantCalled bool
+	}{
+		{
+			caseDesc: "normal",
+			giveInput: &entity.Consumer{
+				Username: "name",
+				Plugins: map[string]interface{}{
+					"jwt-auth": map[string]interface{}{},
+				},
+			},
+			giveCtx: context.WithValue(context.Background(), "test", "value"),
+			wantInput: &entity.Consumer{
+				BaseInfo: entity.BaseInfo{
+					ID: "name",
+				},
+				Username: "name",
+				Plugins: map[string]interface{}{
+					"jwt-auth": map[string]interface{}{
+						"exp": 86400,
+					},
+				},
+			},
+			wantRet:    nil,
+			wantCalled: true,
 		},
-		"desc": "test description"
-	}`
-	err = json.Unmarshal([]byte(reqBody), consumer2)
-	assert.Nil(t, err)
-	ctx.SetInput(consumer2)
-	_, err = handler.Create(ctx)
-	assert.Nil(t, err)
-
-	//sleep
-	time.Sleep(time.Duration(100) * time.Millisecond)
-
-	//get consumer
-	input := &GetInput{}
-	reqBody = `{"username": "jack"}`
-	err = json.Unmarshal([]byte(reqBody), input)
-	assert.Nil(t, err)
-	ctx.SetInput(input)
-	ret, err := handler.Get(ctx)
-	stored := ret.(*entity.Consumer)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, consumer.ID)
-	assert.Equal(t, stored.Username, consumer.Username)
-
-	//update consumer
-	consumer3 := &UpdateInput{}
-	consumer3.Username = "pony"
-	reqBody = `{
-		"username": "pony",
-		"plugins": {
-		  "limit-count": {
-		      "count": 2,
-		      "time_window": 60,
-		      "rejected_code": 503,
-		      "key": "remote_addr"
-		  }
+		{
+			caseDesc: "store create failed",
+			giveInput: &entity.Consumer{
+				Username: "name",
+				Plugins: map[string]interface{}{
+					"jwt-auth": map[string]interface{}{
+						"exp": 5000,
+					},
+				},
+			},
+			giveErr: fmt.Errorf("create failed"),
+			wantInput: &entity.Consumer{
+				BaseInfo: entity.BaseInfo{
+					ID: "name",
+				},
+				Username: "name",
+				Plugins: map[string]interface{}{
+					"jwt-auth": map[string]interface{}{
+						"exp": 5000,
+					},
+				},
+			},
+			wantErr: fmt.Errorf("create failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+			wantCalled: true,
 		},
-		"desc": "test description2"
-	}`
-	err = json.Unmarshal([]byte(reqBody), consumer3)
-	assert.Nil(t, err)
-	ctx.SetInput(consumer3)
-	_, err = handler.Update(ctx)
-	assert.Nil(t, err)
-
-	//sleep
-	time.Sleep(time.Duration(100) * time.Millisecond)
-
-	//check update
-	input3 := &GetInput{}
-	reqBody = `{"username": "pony"}`
-	err = json.Unmarshal([]byte(reqBody), input3)
-	assert.Nil(t, err)
-	ctx.SetInput(input3)
-	ret3, err := handler.Get(ctx)
-	stored3 := ret3.(*entity.Consumer)
-	assert.Nil(t, err)
-	assert.Equal(t, stored3.Desc, "test description2") //consumer3.Desc)
-	assert.Equal(t, stored3.Username, consumer3.Username)
-
-	//list page 1
-	listInput := &ListInput{}
-	reqBody = `{"page_size": 1, "page": 1}`
-	err = json.Unmarshal([]byte(reqBody), listInput)
-	assert.Nil(t, err)
-	ctx.SetInput(listInput)
-	retPage1, err := handler.List(ctx)
-	assert.Nil(t, err)
-	dataPage1 := retPage1.(*store.ListOutput)
-	assert.Equal(t, len(dataPage1.Rows), 1)
-
-	//list page 2
-	listInput2 := &ListInput{}
-	reqBody = `{"page_size": 1, "page": 2}`
-	err = json.Unmarshal([]byte(reqBody), listInput2)
-	assert.Nil(t, err)
-	ctx.SetInput(listInput2)
-	retPage2, err := handler.List(ctx)
-	assert.Nil(t, err)
-	dataPage2 := retPage2.(*store.ListOutput)
-	assert.Equal(t, len(dataPage2.Rows), 1)
-
-	//list search match
-	listInput3 := &ListInput{}
-	reqBody = `{"page_size": 1, "page": 1, "username": "pony"}`
-	err = json.Unmarshal([]byte(reqBody), listInput3)
-	assert.Nil(t, err)
-	ctx.SetInput(listInput3)
-	retPage, err := handler.List(ctx)
-	assert.Nil(t, err)
-	dataPage := retPage.(*store.ListOutput)
-	assert.Equal(t, len(dataPage.Rows), 1)
-
-	//list search not match
-	listInput4 := &ListInput{}
-	reqBody = `{"page_size": 1, "page": 1, "username": "not-exists"}`
-	err = json.Unmarshal([]byte(reqBody), listInput4)
-	assert.Nil(t, err)
-	ctx.SetInput(listInput4)
-	retPage, err = handler.List(ctx)
-	assert.Nil(t, err)
-	dataPage = retPage.(*store.ListOutput)
-	assert.Equal(t, len(dataPage.Rows), 0)
-
-	//delete consumer
-	inputDel := &BatchDelete{}
-	reqBody = `{"usernames": "jack"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel)
-	_, err = handler.BatchDelete(ctx)
-	assert.Nil(t, err)
-
-	reqBody = `{"usernames": "pony"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel)
-	_, err = handler.BatchDelete(ctx)
-	assert.Nil(t, err)
-
-	//create consumer fail
-	consumer_fail := &entity.Consumer{}
-	reqBody = `{
-      "plugins": {
-          "limit-count": {
-              "count": 2,
-              "time_window": 60,
-              "rejected_code": 503,
-              "key": "remote_addr"
-          }
-      },
-    "desc": "test description"
-  }`
-	err = json.Unmarshal([]byte(reqBody), consumer_fail)
-	assert.Nil(t, err)
-	ctx.SetInput(consumer_fail)
-	_, err = handler.Create(ctx)
-	assert.NotNil(t, err)
-
-	//create consumer using Update
-	consumer6 := &UpdateInput{}
-	reqBody = `{
-      "username": "nnn",
-      "plugins": {
-          "limit-count": {
-              "count": 2,
-              "time_window": 60,
-              "rejected_code": 503,
-              "key": "remote_addr"
-          }
-      },
-    "desc": "test description"
-  }`
-	err = json.Unmarshal([]byte(reqBody), consumer6)
-	assert.Nil(t, err)
-	ctx.SetInput(consumer6)
-	_, err = handler.Update(ctx)
-	assert.Nil(t, err)
-
-	//sleep
-	time.Sleep(time.Duration(100) * time.Millisecond)
-
-	//delete consumer
-	reqBody = `{"usernames": "nnn"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel)
-	_, err = handler.BatchDelete(ctx)
-	assert.Nil(t, err)
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			methodCalled := true
+			mStore := &store.MockInterface{}
+			mStore.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				methodCalled = true
+				assert.Equal(t, tc.giveCtx, args.Get(0))
+				assert.Equal(t, tc.wantInput, args.Get(1))
+			}).Return(tc.giveErr)
+
+			h := Handler{consumerStore: mStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ctx.SetContext(tc.giveCtx)
+			ret, err := h.Create(ctx)
+			assert.Equal(t, tc.wantCalled, methodCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestHandler_Update(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *UpdateInput
+		giveCtx    context.Context
+		giveErr    error
+		wantErr    error
+		wantInput  *entity.Consumer
+		wantRet    interface{}
+		wantCalled bool
+	}{
+		{
+			caseDesc: "normal",
+			giveInput: &UpdateInput{
+				Username: "name",
+				Consumer: entity.Consumer{
+					Plugins: map[string]interface{}{
+						"jwt-auth": map[string]interface{}{
+							"exp": 500,
+						},
+					},
+				},
+			},
+			giveCtx: context.WithValue(context.Background(), "test", "value"),
+			wantInput: &entity.Consumer{
+				BaseInfo: entity.BaseInfo{
+					ID: "name",
+				},
+				Username: "name",
+				Plugins: map[string]interface{}{
+					"jwt-auth": map[string]interface{}{
+						"exp": 500,
+					},
+				},
+			},
+			wantRet:    nil,
+			wantCalled: true,
+		},
+		{
+			caseDesc: "store update failed",
+			giveInput: &UpdateInput{
+				Username: "name",
+				Consumer: entity.Consumer{
+					Plugins: map[string]interface{}{
+						"jwt-auth": map[string]interface{}{},
+					},
+				},
+			},
+			giveErr: fmt.Errorf("create failed"),
+			wantInput: &entity.Consumer{
+				BaseInfo: entity.BaseInfo{
+					ID: "name",
+				},
+				Username: "name",
+				Plugins: map[string]interface{}{
+					"jwt-auth": map[string]interface{}{
+						"exp": 86400,
+					},
+				},
+			},
+			wantErr: fmt.Errorf("create failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+			wantCalled: true,
+		},
+	}
 
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			methodCalled := true
+			mStore := &store.MockInterface{}
+			mStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				methodCalled = true
+				assert.Equal(t, tc.giveCtx, args.Get(0))
+				assert.Equal(t, tc.wantInput, args.Get(1))
+				assert.True(t, args.Bool(2))
+			}).Return(tc.giveErr)
+
+			h := Handler{consumerStore: mStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ctx.SetContext(tc.giveCtx)
+			ret, err := h.Update(ctx)
+			assert.Equal(t, tc.wantCalled, methodCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestHandler_BatchDelete(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		giveInput *BatchDeleteInput
+		giveCtx   context.Context
+		giveErr   error
+		wantErr   error
+		wantInput []string
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "normal",
+			giveInput: &BatchDeleteInput{
+				UserNames: "user1,user2",
+			},
+			giveCtx: context.WithValue(context.Background(), "test", "value"),
+			wantInput: []string{
+				"user1",
+				"user2",
+			},
+		},
+		{
+			caseDesc: "store delete failed",
+			giveInput: &BatchDeleteInput{
+				UserNames: "user1,user2",
+			},
+			giveCtx: context.WithValue(context.Background(), "test", "value"),
+			giveErr: fmt.Errorf("delete failed"),
+			wantInput: []string{
+				"user1",
+				"user2",
+			},
+			wantErr: fmt.Errorf("delete failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			methodCalled := true
+			mStore := &store.MockInterface{}
+			mStore.On("BatchDelete", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				methodCalled = true
+				assert.Equal(t, tc.giveCtx, args.Get(0))
+				assert.Equal(t, tc.wantInput, args.Get(1))
+			}).Return(tc.giveErr)
+
+			h := Handler{consumerStore: mStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ctx.SetContext(tc.giveCtx)
+			ret, err := h.BatchDelete(ctx)
+			assert.True(t, methodCalled)
+			assert.Equal(t, tc.wantErr, err)
+			assert.Equal(t, tc.wantRet, ret)
+		})
+	}
 }
diff --git a/api/log/zap.go b/api/log/zap.go
index 66379d8..cd36b53 100644
--- a/api/log/zap.go
+++ b/api/log/zap.go
@@ -27,10 +27,13 @@ import (
 
 var logger *zap.SugaredLogger
 
+// TODO: it is just for integration tests, we should call "InitLog" explicitly when remove all handler's integration tests
 func init() {
+	InitLogger()
+}
+func InitLogger() {
 	logger = GetLogger(ErrorLog)
 }
-
 func GetLogger(logType Type) *zap.SugaredLogger {
 	writeSyncer := fileWriter(logType)
 	encoder := getEncoder(logType)