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 2020/12/31 01:54:20 UTC

[GitHub] [apisix-dashboard] starsz opened a new pull request #1169: feat: remove the etcd dependency in the service unit test

starsz opened a new pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169


   Please answer these questions before submitting a pull request
   
   - Why submit this pull request?
   - [] Bugfix
   - [x] New feature provided
   - [ ] Improve performance
   
   - Related issues
   
   #1161 
   
   ### New feature or improvement
   - Describe the details and related test reports.
   
   Remove the etcd dependency in the service unit test 


----------------------------------------------------------------
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



[GitHub] [apisix-dashboard] starsz merged pull request #1169: feat: remove the etcd dependency in the service unit test

Posted by GitBox <gi...@apache.org>.
starsz merged pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169


   


----------------------------------------------------------------
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



[GitHub] [apisix-dashboard] codecov-io edited a comment on pull request #1169: feat: remove the etcd dependency in the service unit test

Posted by GitBox <gi...@apache.org>.
codecov-io edited a comment on pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#issuecomment-752815085


   # [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=h1) Report
   > Merging [#1169](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=desc) (f8239e2) into [master](https://codecov.io/gh/apache/apisix-dashboard/commit/9824b5f60121f7274c41a93aacff0cc80dec0fa2?el=desc) (9824b5f) will **decrease** coverage by `10.14%`.
   > The diff coverage is `n/a`.
   
   [![Impacted file tree graph](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/graphs/tree.svg?width=650&height=150&src=pr&token=Q1HERXN96P)](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=tree)
   
   ```diff
   @@             Coverage Diff             @@
   ##           master    #1169       +/-   ##
   ===========================================
   - Coverage   67.08%   56.93%   -10.15%     
   ===========================================
     Files          46       46               
     Lines        2947     2947               
   ===========================================
   - Hits         1977     1678      -299     
   - Misses        731     1004      +273     
   - Partials      239      265       +26     
   ```
   
   
   | [Impacted Files](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=tree) | Coverage Δ | |
   |---|---|---|
   | [api/internal/handler/plugin/plugin.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvcGx1Z2luL3BsdWdpbi5nbw==) | `16.66% <0.00%> (-70.00%)` | :arrow_down: |
   | [api/internal/core/store/query.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvc3RvcmUvcXVlcnkuZ28=) | `45.23% <0.00%> (-52.39%)` | :arrow_down: |
   | [api/internal/core/store/selector.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvc3RvcmUvc2VsZWN0b3IuZ28=) | `35.18% <0.00%> (-51.86%)` | :arrow_down: |
   | [api/internal/core/store/validate\_mock.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvc3RvcmUvdmFsaWRhdGVfbW9jay5nbw==) | `0.00% <0.00%> (-50.00%)` | :arrow_down: |
   | [api/internal/filter/authentication.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2ZpbHRlci9hdXRoZW50aWNhdGlvbi5nbw==) | `44.11% <0.00%> (-32.36%)` | :arrow_down: |
   | [api/internal/core/store/store.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvc3RvcmUvc3RvcmUuZ28=) | `56.96% <0.00%> (-29.70%)` | :arrow_down: |
   | [api/internal/handler/global\_rule/global\_rule.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvZ2xvYmFsX3J1bGUvZ2xvYmFsX3J1bGUuZ28=) | `64.51% <0.00%> (-19.36%)` | :arrow_down: |
   | [api/internal/handler/consumer/consumer.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvY29uc3VtZXIvY29uc3VtZXIuZ28=) | `74.46% <0.00%> (-17.03%)` | :arrow_down: |
   | [api/internal/handler/ssl/ssl.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvc3NsL3NzbC5nbw==) | `33.33% <0.00%> (-14.82%)` | :arrow_down: |
   | [api/internal/utils/json\_patch.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL3V0aWxzL2pzb25fcGF0Y2guZ28=) | `44.82% <0.00%> (-13.80%)` | :arrow_down: |
   | ... and [14 more](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree-more) | |
   
   ------
   
   [Continue to review full report at Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=continue).
   > **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
   > `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
   > Powered by [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=footer). Last update [9824b5f...f8239e2](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).
   


----------------------------------------------------------------
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



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

Posted by GitBox <gi...@apache.org>.
juzhiyuan commented on pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#issuecomment-773114597






----------------------------------------------------------------
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



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

Posted by GitBox <gi...@apache.org>.
nic-chen commented on a change in pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#discussion_r570015980



##########
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:
       OK




----------------------------------------------------------------
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



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

Posted by GitBox <gi...@apache.org>.
juzhiyuan commented on pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#issuecomment-773114764


   @starsz You may need to request reviews from others first.


----------------------------------------------------------------
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



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

Posted by GitBox <gi...@apache.org>.
starsz commented on a change in pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#discussion_r569993804



##########
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:
       It's hard to test this.
   Because we don't call `upstream.Get` in the `Update` function.
   What we can test is that the third argument in the `Update` function is true.

##########
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:
       Fixed.

##########
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:
       Added.




----------------------------------------------------------------
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



[GitHub] [apisix-dashboard] codecov-io commented on pull request #1169: feat: remove the etcd dependency in the service unit test

Posted by GitBox <gi...@apache.org>.
codecov-io commented on pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#issuecomment-752815085


   # [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=h1) Report
   > Merging [#1169](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=desc) (515edad) into [master](https://codecov.io/gh/apache/apisix-dashboard/commit/45d79b674c2f500d76b76939674b6792201a70bb?el=desc) (45d79b6) will **increase** coverage by `1.26%`.
   > The diff coverage is `n/a`.
   
   [![Impacted file tree graph](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/graphs/tree.svg?width=650&height=150&src=pr&token=Q1HERXN96P)](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=tree)
   
   ```diff
   @@            Coverage Diff             @@
   ##           master    #1169      +/-   ##
   ==========================================
   + Coverage   42.30%   43.56%   +1.26%     
   ==========================================
     Files          31       31              
     Lines        1924     1928       +4     
   ==========================================
   + Hits          814      840      +26     
   + Misses       1000      983      -17     
   + Partials      110      105       -5     
   ```
   
   
   | [Impacted Files](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=tree) | Coverage Δ | |
   |---|---|---|
   | [api/internal/handler/global\_rule/global\_rule.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvZ2xvYmFsX3J1bGUvZ2xvYmFsX3J1bGUuZ28=) | `61.40% <0.00%> (-0.87%)` | :arrow_down: |
   | [api/internal/handler/service/service.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvc2VydmljZS9zZXJ2aWNlLmdv) | `52.08% <0.00%> (+24.99%)` | :arrow_up: |
   
   ------
   
   [Continue to review full report at Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=continue).
   > **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
   > `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
   > Powered by [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=footer). Last update [45d79b6...515edad](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).
   


----------------------------------------------------------------
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



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

Posted by GitBox <gi...@apache.org>.
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



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

Posted by GitBox <gi...@apache.org>.
juzhiyuan commented on pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#issuecomment-773166020


   may sync the latest codes to fix E2E failed.


----------------------------------------------------------------
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



[GitHub] [apisix-dashboard] codecov-io edited a comment on pull request #1169: feat: remove the etcd dependency in the service unit test

Posted by GitBox <gi...@apache.org>.
codecov-io edited a comment on pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#issuecomment-752815085


   # [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=h1) Report
   > Merging [#1169](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=desc) (f8239e2) into [master](https://codecov.io/gh/apache/apisix-dashboard/commit/9824b5f60121f7274c41a93aacff0cc80dec0fa2?el=desc) (9824b5f) will **decrease** coverage by `10.14%`.
   > The diff coverage is `n/a`.
   
   [![Impacted file tree graph](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/graphs/tree.svg?width=650&height=150&src=pr&token=Q1HERXN96P)](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=tree)
   
   ```diff
   @@             Coverage Diff             @@
   ##           master    #1169       +/-   ##
   ===========================================
   - Coverage   67.08%   56.93%   -10.15%     
   ===========================================
     Files          46       46               
     Lines        2947     2947               
   ===========================================
   - Hits         1977     1678      -299     
   - Misses        731     1004      +273     
   - Partials      239      265       +26     
   ```
   
   
   | [Impacted Files](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=tree) | Coverage Δ | |
   |---|---|---|
   | [api/internal/handler/plugin/plugin.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvcGx1Z2luL3BsdWdpbi5nbw==) | `16.66% <0.00%> (-70.00%)` | :arrow_down: |
   | [api/internal/core/store/query.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvc3RvcmUvcXVlcnkuZ28=) | `45.23% <0.00%> (-52.39%)` | :arrow_down: |
   | [api/internal/core/store/selector.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvc3RvcmUvc2VsZWN0b3IuZ28=) | `35.18% <0.00%> (-51.86%)` | :arrow_down: |
   | [api/internal/core/store/validate\_mock.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvc3RvcmUvdmFsaWRhdGVfbW9jay5nbw==) | `0.00% <0.00%> (-50.00%)` | :arrow_down: |
   | [api/internal/filter/authentication.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2ZpbHRlci9hdXRoZW50aWNhdGlvbi5nbw==) | `44.11% <0.00%> (-32.36%)` | :arrow_down: |
   | [api/internal/core/store/store.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvc3RvcmUvc3RvcmUuZ28=) | `56.96% <0.00%> (-29.70%)` | :arrow_down: |
   | [api/internal/handler/global\_rule/global\_rule.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvZ2xvYmFsX3J1bGUvZ2xvYmFsX3J1bGUuZ28=) | `64.51% <0.00%> (-19.36%)` | :arrow_down: |
   | [api/internal/handler/consumer/consumer.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvY29uc3VtZXIvY29uc3VtZXIuZ28=) | `74.46% <0.00%> (-17.03%)` | :arrow_down: |
   | [api/internal/handler/ssl/ssl.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvc3NsL3NzbC5nbw==) | `33.33% <0.00%> (-14.82%)` | :arrow_down: |
   | [api/internal/utils/json\_patch.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL3V0aWxzL2pzb25fcGF0Y2guZ28=) | `44.82% <0.00%> (-13.80%)` | :arrow_down: |
   | ... and [14 more](https://codecov.io/gh/apache/apisix-dashboard/pull/1169/diff?src=pr&el=tree-more) | |
   
   ------
   
   [Continue to review full report at Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=continue).
   > **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
   > `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
   > Powered by [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=footer). Last update [9824b5f...f8239e2](https://codecov.io/gh/apache/apisix-dashboard/pull/1169?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).
   


----------------------------------------------------------------
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



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

Posted by GitBox <gi...@apache.org>.
nic-chen commented on a change in pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#discussion_r570015980



##########
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:
       OK




----------------------------------------------------------------
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



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

Posted by GitBox <gi...@apache.org>.
juzhiyuan commented on pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#issuecomment-773114597


   waiting for #1426 


----------------------------------------------------------------
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



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

Posted by GitBox <gi...@apache.org>.
starsz commented on a change in pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#discussion_r570000654



##########
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:
       Added.




----------------------------------------------------------------
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



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

Posted by GitBox <gi...@apache.org>.
juzhiyuan commented on pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#issuecomment-772419999


   @starsz you need to request members to review.


----------------------------------------------------------------
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



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

Posted by GitBox <gi...@apache.org>.
starsz commented on a change in pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#discussion_r569993804



##########
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:
       It's hard to test this.
   Because we don't call `upstream.Get` in the `Update` function.
   What we can test is that the third argument in the `Update` function is true.




----------------------------------------------------------------
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



[GitHub] [apisix-dashboard] starsz merged pull request #1169: feat: remove the etcd dependency in the service unit test

Posted by GitBox <gi...@apache.org>.
starsz merged pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169


   


----------------------------------------------------------------
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



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

Posted by GitBox <gi...@apache.org>.
starsz commented on a change in pull request #1169:
URL: https://github.com/apache/apisix-dashboard/pull/1169#discussion_r570000435



##########
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:
       Fixed.




----------------------------------------------------------------
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