You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by GitBox <gi...@apache.org> on 2021/02/04 06:11:08 UTC

[GitHub] [apisix-dashboard] nic-chen commented on a change in pull request #1169: feat: remove the etcd dependency in the service unit test

nic-chen commented on a change in pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#discussion_r569968494



##########
File path: api/internal/handler/service/service_test.go
##########
@@ -18,274 +18,832 @@
 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 name success",

Review comment:
       desc is not clear.

##########
File path: api/internal/handler/service/service_test.go
##########
@@ -18,274 +18,832 @@
 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",

Review comment:
       we could add a test case for part data.

##########
File path: api/internal/handler/service/service_test.go
##########
@@ -18,274 +18,832 @@
 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"),
+		},
+		{

Review comment:
       we could add a test case for service not found.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org