You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by st...@apache.org on 2021/02/05 04:05:28 UTC

[apisix-dashboard] branch master updated: feat: remove the etcd dependency in the service unit test

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

starsz 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 661e48f  feat: remove the etcd dependency in the service unit test
661e48f is described below

commit 661e48fb8cd1035429bd4d14919f7b8ea910d5ad
Author: Peter Zhu <st...@gmail.com>
AuthorDate: Fri Feb 5 12:05:22 2021 +0800

    feat: remove the etcd dependency in the service unit test
---
 api/internal/handler/service/service_test.go | 1037 ++++++++++++++++++++------
 1 file changed, 801 insertions(+), 236 deletions(-)

diff --git a/api/internal/handler/service/service_test.go b/api/internal/handler/service/service_test.go
index 4f469af..40da731 100644
--- a/api/internal/handler/service/service_test.go
+++ b/api/internal/handler/service/service_test.go
@@ -18,274 +18,839 @@
 package service
 
 import (
-	"encoding/json"
-	"strings"
+	"fmt"
+	"net/http"
 	"testing"
-	"time"
 
 	"github.com/shiningrush/droplet"
+	"github.com/shiningrush/droplet/data"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
-	"github.com/apisix/manager-api/internal/conf"
 	"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/apisix/manager-api/internal/handler"
 )
 
-func TestService(t *testing.T) {
-	// init
-	err := storage.InitETCDClient(conf.ETCDConfig)
-	assert.Nil(t, err)
-	err = store.InitStores()
-	assert.Nil(t, err)
-
-	handler := &Handler{
-		serviceStore: store.GetStore(store.HubKeyService),
+func TestService_Get(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *GetInput
+		giveRet    *entity.Service
+		giveErr    error
+		wantErr    error
+		wantGetKey string
+		wantRet    interface{}
+	}{
+		{
+			caseDesc:   "normal",
+			giveInput:  &GetInput{ID: "s1"},
+			wantGetKey: "s1",
+			giveRet: &entity.Service{
+				BaseInfo: entity.BaseInfo{
+					ID: "s1",
+				},
+				Plugins: map[string]interface{}{
+					"limit-count": map[string]interface{}{
+						"count":         2,
+						"time_window":   60,
+						"rejected_code": 503,
+						"key":           "remote_addr",
+					},
+				},
+			},
+			wantRet: &entity.Service{
+				BaseInfo: entity.BaseInfo{
+					ID: "s1",
+				},
+				Plugins: map[string]interface{}{
+					"limit-count": map[string]interface{}{
+						"count":         2,
+						"time_window":   60,
+						"rejected_code": 503,
+						"key":           "remote_addr",
+					},
+				},
+			},
+		},
+		{
+			caseDesc:   "store get failed",
+			giveInput:  &GetInput{ID: "failed_key"},
+			wantGetKey: "failed_key",
+			giveErr:    fmt.Errorf("get failed"),
+			wantErr:    fmt.Errorf("get failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
 	}
-	assert.NotNil(t, handler)
 
-	//create
-	ctx := droplet.NewContext()
-	service := &entity.Service{}
-	reqBody := `{
-      "id": "1",
-      "plugins": {
-          "limit-count": {
-              "count": 2,
-              "time_window": 60,
-              "rejected_code": 503,
-              "key": "remote_addr"
-          }
-      },
-      "upstream": {
-          "type": "roundrobin",
-          "nodes": [{
-              "host": "39.97.63.215",
-              "port": 80,
-              "weight": 1
-          }]
-      }
-  }`
-	err = json.Unmarshal([]byte(reqBody), service)
-	assert.Nil(t, err)
-	ctx.SetInput(service)
-	ret, err := handler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Service)
-	assert.True(t, ok)
-	assert.Equal(t, "1", objRet.ID)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			mStore := &store.MockInterface{}
+			mStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				assert.Equal(t, tc.wantGetKey, args.Get(0))
+			}).Return(tc.giveRet, tc.giveErr)
 
-	//sleep
-	time.Sleep(time.Duration(100) * time.Millisecond)
-
-	//get
-	input := &GetInput{}
-	input.ID = "1"
-	ctx.SetInput(input)
-	ret, err = handler.Get(ctx)
-	stored := ret.(*entity.Service)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, service.ID)
+			h := Handler{serviceStore: 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)
+		})
+	}
+}
 
-	//update
-	service2 := &UpdateInput{}
-	service2.ID = "1"
-	reqBody = `{
-		"name": "test-service",
-		"plugins": {
-		  "limit-count": {
-		      "count": 2,
-		      "time_window": 60,
-		      "rejected_code": 503,
-		      "key": "remote_addr"
-		  }
+func TestService_List(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		giveInput *ListInput
+		giveData  []*entity.Service
+		giveErr   error
+		wantErr   error
+		wantInput store.ListInput
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "list all service",
+			giveInput: &ListInput{
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			giveData: []*entity.Service{
+				{Name: "s1"},
+				{Name: "s2"},
+				{Name: "test_service"},
+				{Name: "service_test"},
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					&entity.Service{Name: "s1"},
+					&entity.Service{Name: "s2"},
+					&entity.Service{Name: "test_service"},
+					&entity.Service{Name: "service_test"},
+				},
+				TotalSize: 4,
+			},
 		},
-		"enable_websocket": true,
-		"upstream": {
-		  "type": "roundrobin",
-		  "nodes": [{
-		      "host": "39.97.63.215",
-		      "port": 80,
-		      "weight": 1
-		  }]
-		}
-	}`
-	err = json.Unmarshal([]byte(reqBody), service2)
-	assert.Nil(t, err)
-	ctx.SetInput(service2)
-	ret, err = handler.Update(ctx)
-	assert.Nil(t, err)
-	// Check the returned value
-	objRet, ok = ret.(*entity.Service)
-	assert.True(t, ok)
-	assert.Equal(t, service2.ID, objRet.ID)
-	assert.Equal(t, service2.Name, objRet.Name)
+		{
+			caseDesc: "list service with 'service'",
+			giveInput: &ListInput{
+				Name: "service",
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			giveData: []*entity.Service{
+				{BaseInfo: entity.BaseInfo{CreateTime: 1609376661}, Name: "s1"},
+				{BaseInfo: entity.BaseInfo{CreateTime: 1609376662}, Name: "s2"},
+				{BaseInfo: entity.BaseInfo{CreateTime: 1609376663}, Name: "test_service"},
+				{BaseInfo: entity.BaseInfo{CreateTime: 1609376664}, Name: "service_test"},
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					&entity.Service{BaseInfo: entity.BaseInfo{CreateTime: 1609376663}, Name: "test_service"},
+					&entity.Service{BaseInfo: entity.BaseInfo{CreateTime: 1609376664}, Name: "service_test"},
+				},
+				TotalSize: 2,
+			},
+		},
+		{
+			caseDesc: "list service with key s1",
+			giveInput: &ListInput{
+				Name: "s1",
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			giveData: []*entity.Service{
+				{Name: "s1"},
+				{Name: "s2"},
+				{Name: "test_service"},
+				{Name: "service_test"},
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					&entity.Service{Name: "s1"},
+				},
+				TotalSize: 1,
+			},
+		},
+		{
+			caseDesc: "list service and format",
+			giveInput: &ListInput{
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			giveData: []*entity.Service{
+				{
+					Name: "s1",
+					Upstream: &entity.UpstreamDef{
+						Nodes: []interface{}{
+							map[string]interface{}{
+								"host":   "39.97.63.215",
+								"port":   float64(80),
+								"weight": float64(1),
+							},
+						},
+					},
+				},
+				{Name: "s2"},
+				{Name: "test_service"},
+				{Name: "service_test"},
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					&entity.Service{Name: "s1", Upstream: &entity.UpstreamDef{
+						Nodes: []*entity.Node{
+							{
+								Host:   "39.97.63.215",
+								Port:   80,
+								Weight: 1,
+							},
+						},
+					}},
+					&entity.Service{Name: "s2"},
+					&entity.Service{Name: "test_service"},
+					&entity.Service{Name: "service_test"},
+				},
+				TotalSize: 4,
+			},
+		},
+	}
 
-	//sleep
-	time.Sleep(time.Duration(100) * time.Millisecond)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			mStore := &store.MockInterface{}
+			mStore.On("List", mock.Anything, 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) {
+						if input.Format == nil {
+							returnData = append(returnData, c)
+							continue
+						}
 
-	//list
-	listInput := &ListInput{}
-	reqBody = `{"page_size": 1, "page": 1}`
-	err = json.Unmarshal([]byte(reqBody), listInput)
-	assert.Nil(t, err)
-	ctx.SetInput(listInput)
-	retPage, err := handler.List(ctx)
-	assert.Nil(t, err)
-	dataPage := retPage.(*store.ListOutput)
-	assert.Equal(t, len(dataPage.Rows), 1)
+						returnData = append(returnData, input.Format(c))
+					}
+				}
+				return &store.ListOutput{
+					Rows:      returnData,
+					TotalSize: len(returnData),
+				}
+			}, tc.giveErr)
 
-	//list search match
-	listInput2 := &ListInput{}
-	reqBody = `{"page_size": 1, "page": 1, "name": "test"}`
-	err = json.Unmarshal([]byte(reqBody), listInput2)
-	assert.Nil(t, err)
-	ctx.SetInput(listInput2)
-	retPage, err = handler.List(ctx)
-	assert.Nil(t, err)
-	dataPage = retPage.(*store.ListOutput)
-	assert.Equal(t, len(dataPage.Rows), 1)
+			h := Handler{serviceStore: 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)
+		})
+	}
+}
 
-	//list search not match
-	listInput3 := &ListInput{}
-	reqBody = `{"page_size": 1, "page": 1, "name": "not-exists"}`
-	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), 0)
+func TestService_Create(t *testing.T) {
+	tests := []struct {
+		caseDesc      string
+		getCalled     bool
+		giveInput     *entity.Service
+		giveRet       interface{}
+		giveErr       error
+		wantInput     *entity.Service
+		wantErr       error
+		wantRet       interface{}
+		upstreamInput string
+		upstreamRet   interface{}
+		upstreamErr   interface{}
+	}{
+		{
+			caseDesc:  "create success",
+			getCalled: true,
+			giveInput: &entity.Service{
+				Name:       "s1",
+				UpstreamID: "u1",
+				Desc:       "test service",
+			},
+			wantInput: &entity.Service{
+				Name:       "s1",
+				UpstreamID: "u1",
+				Desc:       "test service",
+			},
+			upstreamInput: "u1",
+			upstreamRet: entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+			},
+		},
+		{
+			caseDesc:  "create failed, upstream not found",
+			getCalled: false,
+			giveInput: &entity.Service{
+				Name:       "s1",
+				UpstreamID: "u1",
+				Desc:       "test service",
+			},
+			wantInput: &entity.Service{
+				Name:       "s1",
+				UpstreamID: "u1",
+				Desc:       "test service",
+			},
+			wantErr:       fmt.Errorf("upstream id: u1 not found"),
+			wantRet:       &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
+			upstreamInput: "u1",
+			upstreamErr:   data.ErrNotFound,
+		},
+		{
+			caseDesc:  "create failed, upstream return error",
+			getCalled: false,
+			giveInput: &entity.Service{
+				Name:       "s1",
+				UpstreamID: "u1",
+				Desc:       "test service",
+			},
+			wantInput: &entity.Service{
+				Name:       "s1",
+				UpstreamID: "u1",
+				Desc:       "test service",
+			},
+			wantErr:       fmt.Errorf("unknown error"),
+			wantRet:       &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
+			upstreamInput: "u1",
+			upstreamErr:   fmt.Errorf("unknown error"),
+		},
+		{
+			caseDesc:  "create failed, create return error",
+			getCalled: true,
+			giveInput: &entity.Service{
+				Name:       "s1",
+				UpstreamID: "u1",
+				Desc:       "test service",
+			},
+			giveErr: fmt.Errorf("create failed"),
+			wantInput: &entity.Service{
+				Name:       "s1",
+				UpstreamID: "u1",
+				Desc:       "test service",
+			},
+			upstreamInput: "u1",
+			upstreamRet: entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+			},
+			wantErr: fmt.Errorf("create failed"),
+			wantRet: handler.SpecCodeResponse(fmt.Errorf("create failed")),
+		},
+	}
 
-	//delete test data
-	inputDel := &BatchDelete{}
-	reqBody = `{"ids": "1"}`
-	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) {
+			getCalled := false
+			serviceStore := &store.MockInterface{}
+			serviceStore.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Service)
+				assert.Equal(t, tc.wantInput, input)
+			}).Return(tc.giveRet, tc.giveErr)
 
-	//create without upstream
-	service11 := &entity.Service{}
-	reqBody = `{
-      "id": "11",
-      "plugins": {
-          "limit-count": {
-              "count": 2,
-              "time_window": 60,
-              "rejected_code": 503,
-              "key": "remote_addr"
-          }
-      }
-  }`
-	err = json.Unmarshal([]byte(reqBody), service11)
-	assert.Nil(t, err)
-	ctx.SetInput(service11)
-	ret, err = handler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok = ret.(*entity.Service)
-	assert.True(t, ok)
-	assert.Equal(t, "11", objRet.ID)
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				id := args.Get(0).(string)
+				assert.Equal(t, tc.upstreamInput, id)
+			}).Return(tc.upstreamRet, tc.upstreamErr)
 
-	//sleep
-	time.Sleep(time.Duration(100) * time.Millisecond)
+			h := Handler{serviceStore: serviceStore, upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Create(ctx)
+			assert.Equal(t, tc.getCalled, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
 
-	//get
-	input11 := &GetInput{}
-	input11.ID = "11"
-	ctx.SetInput(input11)
-	ret, err = handler.Get(ctx)
-	stored = ret.(*entity.Service)
-	assert.Nil(t, err)
-	assert.Equal(t, "11", stored.ID)
+func TestService_Update(t *testing.T) {
+	tests := []struct {
+		caseDesc      string
+		getCalled     bool
+		giveInput     *UpdateInput
+		giveErr       error
+		giveRet       interface{}
+		wantInput     *entity.Service
+		wantErr       error
+		wantRet       interface{}
+		upstreamInput string
+		upstreamRet   interface{}
+		upstreamErr   interface{}
+	}{
+		{
+			caseDesc:  "create success",
+			getCalled: true,
+			giveInput: &UpdateInput{
+				ID: "s1",
+				Service: entity.Service{
+					Name:       "s1",
+					UpstreamID: "u1",
+					Desc:       "test service",
+				},
+			},
+			wantInput: &entity.Service{
+				BaseInfo: entity.BaseInfo{
+					ID: "s1",
+				},
+				Name:       "s1",
+				UpstreamID: "u1",
+				Desc:       "test service",
+			},
+			upstreamInput: "u1",
+			upstreamRet: entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+			},
+		},
+		{
+			caseDesc: "create failed, different id",
+			giveInput: &UpdateInput{
+				ID: "s1",
+				Service: entity.Service{
+					BaseInfo: entity.BaseInfo{
+						ID: "s2",
+					},
+					Name:       "s1",
+					UpstreamID: "u1",
+					Desc:       "test service",
+				},
+			},
+			wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
+			wantErr: fmt.Errorf("ID on path (s1) doesn't match ID on body (s2)"),
+		},
+		{
+			caseDesc: "update failed, upstream not found",
+			giveInput: &UpdateInput{
+				ID: "s1",
+				Service: entity.Service{
+					Name:       "s1",
+					UpstreamID: "u1",
+					Desc:       "test service",
+				},
+			},
+			wantInput: &entity.Service{
+				Name:       "s1",
+				UpstreamID: "u1",
+				Desc:       "test service",
+			},
+			wantErr:       fmt.Errorf("upstream id: u1 not found"),
+			wantRet:       &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
+			upstreamInput: "u1",
+			upstreamErr:   data.ErrNotFound,
+		},
+		{
+			caseDesc: "update failed, upstream return error",
+			giveInput: &UpdateInput{
+				ID: "s1",
+				Service: entity.Service{
+					Name:       "s1",
+					UpstreamID: "u1",
+					Desc:       "test service",
+				},
+			},
+			wantInput: &entity.Service{
+				Name:       "s1",
+				UpstreamID: "u1",
+				Desc:       "test service",
+			},
+			wantErr:       fmt.Errorf("unknown error"),
+			wantRet:       &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
+			upstreamInput: "u1",
+			upstreamErr:   fmt.Errorf("unknown error"),
+		},
+		{
+			caseDesc:  "update failed, update return error",
+			getCalled: true,
+			giveInput: &UpdateInput{
+				ID: "s1",
+				Service: entity.Service{
+					Name:       "s1",
+					UpstreamID: "u1",
+					Desc:       "test service",
+				},
+			},
+			giveErr:       fmt.Errorf("update failed"),
+			upstreamInput: "u1",
+			upstreamRet: entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+			},
+			wantInput: &entity.Service{
+				BaseInfo:   entity.BaseInfo{ID: "s1"},
+				Name:       "s1",
+				UpstreamID: "u1",
+				Desc:       "test service",
+			},
+			wantErr: fmt.Errorf("update failed"),
+			wantRet: handler.SpecCodeResponse(fmt.Errorf("update failed")),
+		},
+	}
 
-	//list
-	listInput11 := &ListInput{}
-	reqBody = `{"page_size": 10, "page": 1}`
-	err = json.Unmarshal([]byte(reqBody), listInput11)
-	assert.Nil(t, err)
-	ctx.SetInput(listInput11)
-	_, err = handler.List(ctx)
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+			serviceStore := &store.MockInterface{}
+			serviceStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Service)
+				createIfNotExist := args.Get(2).(bool)
+				assert.Equal(t, tc.wantInput, input)
+				assert.True(t, createIfNotExist)
+			}).Return(tc.giveRet, tc.giveErr)
 
-	//delete test data
-	inputDel11 := &BatchDelete{}
-	reqBody = `{"ids": "11"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel11)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel11)
-	_, err = handler.BatchDelete(ctx)
-	assert.Nil(t, err)
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				id := args.Get(0).(string)
+				assert.Equal(t, tc.upstreamInput, id)
+			}).Return(tc.upstreamRet, tc.upstreamErr)
 
+			h := Handler{serviceStore: serviceStore, upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Update(ctx)
+			assert.Equal(t, tc.getCalled, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
 }
 
-func TestService_Patch_Update(t *testing.T) {
-	//create
-	handler := &Handler{
-		serviceStore: store.GetStore(store.HubKeyService),
+func TestService_Patch(t *testing.T) {
+	existService := &entity.Service{
+		BaseInfo: entity.BaseInfo{
+			ID:         "s1",
+			CreateTime: 1609340491,
+			UpdateTime: 1609340491,
+		},
+		Name:            "exist_service",
+		UpstreamID:      "u1",
+		EnableWebsocket: false,
+		Labels: map[string]string{
+			"version": "v1",
+		},
+		Plugins: map[string]interface{}{
+			"limit-count": map[string]interface{}{
+				"count":         2,
+				"time_window":   60,
+				"rejected_code": 503,
+				"key":           "remote_addr",
+			},
+		},
+	}
+
+	tests := []struct {
+		caseDesc     string
+		giveInput    *PatchInput
+		giveErr      error
+		giveRet      interface{}
+		wantInput    *entity.Service
+		wantErr      error
+		wantRet      interface{}
+		serviceInput string
+		serviceRet   *entity.Service
+		serviceErr   error
+		called       bool
+	}{
+		{
+			caseDesc: "patch all success",
+			giveInput: &PatchInput{
+				ID:      "s1",
+				SubPath: "",
+				Body: []byte(`{
+						"name":"patched",
+						"upstream_id":"u2",
+						"enable_websocket":true,
+						"labels":{
+							"version":"v1",
+							"build":"16"
+						},
+						"plugins":{
+							"limit-count":{
+								"count":2,
+								"time_window":60,
+								"rejected_code": 504,
+								"key":"remote_addr"
+							},
+							"key-auth":{
+								"key":"auth-one"
+							}
+						}
+					}`),
+			},
+			wantInput: &entity.Service{
+				BaseInfo: entity.BaseInfo{
+					ID:         "s1",
+					CreateTime: 1609340491,
+					UpdateTime: 1609340491,
+				},
+				Name:            "patched",
+				UpstreamID:      "u2",
+				EnableWebsocket: true,
+				Labels: map[string]string{
+					"version": "v1",
+					"build":   "16",
+				},
+				Plugins: map[string]interface{}{
+					"limit-count": map[string]interface{}{
+						"count":         float64(2),
+						"time_window":   float64(60),
+						"rejected_code": float64(504),
+						"key":           "remote_addr",
+					},
+					"key-auth": map[string]interface{}{
+						"key": "auth-one",
+					},
+				},
+			},
+			serviceInput: "s1",
+			serviceRet:   existService,
+			called:       true,
+		},
+		{
+			caseDesc: "patch part of service success",
+			giveInput: &PatchInput{
+				ID:      "s1",
+				SubPath: "",
+				Body: []byte(`{
+						"name":"patched",
+						"upstream_id":"u2",
+						"enable_websocket":true,
+						"labels":{
+							"version":"v1",
+							"build":"16"
+						}
+					}`),
+			},
+			wantInput: &entity.Service{
+				BaseInfo: entity.BaseInfo{
+					ID:         "s1",
+					CreateTime: 1609340491,
+					UpdateTime: 1609340491,
+				},
+				Name:            "patched",
+				UpstreamID:      "u2",
+				EnableWebsocket: true,
+				Labels: map[string]string{
+					"version": "v1",
+					"build":   "16",
+				},
+				Plugins: map[string]interface{}{
+					"limit-count": map[string]interface{}{
+						"count":         float64(2),
+						"time_window":   float64(60),
+						"rejected_code": float64(503),
+						"key":           "remote_addr",
+					},
+				},
+			},
+			serviceInput: "s1",
+			serviceRet:   existService,
+			called:       true,
+		},
+		{
+			caseDesc: "patch name success with sub path",
+			giveInput: &PatchInput{
+				ID:      "s1",
+				SubPath: "/upstream_id",
+				Body:    []byte(`{"upstream_id":"u3"}`),
+			},
+			wantInput: &entity.Service{
+				BaseInfo: entity.BaseInfo{
+					ID:         "s1",
+					CreateTime: 1609340491,
+					UpdateTime: 1609340491,
+				},
+				Name: "exist_service",
+				UpstreamID: map[string]interface{}{
+					"upstream_id": "u3",
+				},
+				EnableWebsocket: false,
+				Labels: map[string]string{
+					"version": "v1",
+				},
+				Plugins: map[string]interface{}{
+					"limit-count": map[string]interface{}{
+						"count":         float64(2),
+						"time_window":   float64(60),
+						"rejected_code": float64(503),
+						"key":           "remote_addr",
+					},
+				},
+			},
+			serviceInput: "s1",
+			serviceRet:   existService,
+			called:       true,
+		},
+		{
+			caseDesc: "patch labels success",
+			giveInput: &PatchInput{
+				ID:      "s1",
+				SubPath: "/labels",
+				Body:    []byte(`{"version": "v3"}`),
+			},
+			wantInput: &entity.Service{
+				BaseInfo: entity.BaseInfo{
+					ID:         "s1",
+					CreateTime: 1609340491,
+					UpdateTime: 1609340491,
+				},
+				Name:            "exist_service",
+				EnableWebsocket: false,
+				Labels: map[string]string{
+					"version": "v3",
+				},
+				UpstreamID: "u1",
+				Plugins: map[string]interface{}{
+					"limit-count": map[string]interface{}{
+						"count":         float64(2),
+						"time_window":   float64(60),
+						"rejected_code": float64(503),
+						"key":           "remote_addr",
+					},
+				},
+			},
+			serviceInput: "s1",
+			serviceRet:   existService,
+			called:       true,
+		},
+		{
+			caseDesc: "patch failed, service store get error",
+			giveInput: &PatchInput{
+				ID:   "s1",
+				Body: []byte{},
+			},
+			serviceInput: "s1",
+			serviceErr:   fmt.Errorf("get error"),
+			wantRet:      handler.SpecCodeResponse(fmt.Errorf("get error")),
+			wantErr:      fmt.Errorf("get error"),
+			called:       false,
+		},
 	}
-	ctx := droplet.NewContext()
-	service := &entity.Service{}
-	reqBody := `{
-		"id": "3",
-		"name": "testservice",
-		"upstream": {
-			"type": "roundrobin",
-			"nodes": [{
-				"host": "172.16.238.20",
-				"port": 1980,
-				"weight": 1
-			}]
-		}
-	}`
-	err := json.Unmarshal([]byte(reqBody), service)
-	assert.Nil(t, err)
-	ctx.SetInput(service)
-	ret, err := handler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Service)
-	assert.True(t, ok)
-	assert.Equal(t, "3", objRet.ID)
 
-	//sleep
-	time.Sleep(time.Duration(20) * time.Millisecond)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+			serviceStore := &store.MockInterface{}
+			serviceStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Service)
+				createIfNotExist := args.Get(2).(bool)
+				assert.Equal(t, tc.wantInput, input)
+				assert.False(t, createIfNotExist)
+			}).Return(tc.giveRet, tc.giveErr)
 
-	reqBody1 := `{
-		"id": "3",
-		"name": "testpatch",
-		"upstream": {
-			"type": "roundrobin",
-			"nodes": [{
-				"host": "172.16.238.20",
-				"port": 1981,
-				"weight": 1
-			}]
-		}
-	}`
-	responesBody := `"nodes":[{"host":"172.16.238.20","port":1981,"weight":1}],"type":"roundrobin"}`
+			serviceStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				input := args.Get(0).(string)
+				assert.Equal(t, tc.serviceInput, input)
+			}).Return(tc.serviceRet, tc.serviceErr)
 
-	input2 := &PatchInput{}
-	input2.ID = "3"
-	input2.SubPath = ""
-	input2.Body = []byte(reqBody1)
-	ctx.SetInput(input2)
+			h := Handler{serviceStore: serviceStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Patch(ctx)
+			assert.Equal(t, tc.called, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
 
-	ret2, err := handler.Patch(ctx)
-	assert.Nil(t, err)
-	_ret2, err := json.Marshal(ret2)
-	assert.Nil(t, err)
-	isContains := strings.Contains(string(_ret2), responesBody)
-	assert.True(t, isContains)
+func TestServices_Delete(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		giveInput *BatchDelete
+		giveErr   error
+		wantInput []string
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "delete success",
+			giveInput: &BatchDelete{
+				IDs: "s1",
+			},
+			wantInput: []string{"s1"},
+		},
+		{
+			caseDesc: "batch delete success",
+			giveInput: &BatchDelete{
+				IDs: "s1,s2",
+			},
+			wantInput: []string{"s1", "s2"},
+		},
+		{
+			caseDesc: "delete failed",
+			giveInput: &BatchDelete{
+				IDs: "s1",
+			},
+			giveErr:   fmt.Errorf("delete error"),
+			wantInput: []string{"s1"},
+			wantRet:   handler.SpecCodeResponse(fmt.Errorf("delete error")),
+			wantErr:   fmt.Errorf("delete error"),
+		},
+	}
 
-	//delete test data
-	inputDel2 := &BatchDelete{}
-	reqBody = `{"ids": "3"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel2)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel2)
-	_, err = handler.BatchDelete(ctx)
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+			serviceStore := &store.MockInterface{}
+			serviceStore.On("BatchDelete", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).([]string)
+				assert.Equal(t, tc.wantInput, input)
+			}).Return(tc.giveErr)
 
+			h := Handler{serviceStore: serviceStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.BatchDelete(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
 }
-