From 94019c1e1d04fdf25d89d4c8ca69196f5e742bc2 Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Sat, 26 Dec 2020 21:03:22 +0800 Subject: [PATCH] Optimize mock --- .../mock/response.go => http_response.go} | 5 +- client/httplib/httplib.go | 1 + client/httplib/mock.go | 71 ++++++++++++++++++ .../mock/condition.go => mock_condition.go} | 22 +++--- ...ndition_test.go => mock_condition_test.go} | 27 ++++--- .../{filter/mock/filter.go => mock_filter.go} | 41 +++++----- .../filter_test.go => mock_filter_test.go} | 13 ++-- client/httplib/mock_test.go | 75 +++++++++++++++++++ 8 files changed, 200 insertions(+), 55 deletions(-) rename client/httplib/{filter/mock/response.go => http_response.go} (86%) create mode 100644 client/httplib/mock.go rename client/httplib/{filter/mock/condition.go => mock_condition.go} (89%) rename client/httplib/{filter/mock/condition_test.go => mock_condition_test.go} (74%) rename client/httplib/{filter/mock/filter.go => mock_filter.go} (65%) rename client/httplib/{filter/mock/filter_test.go => mock_filter_test.go} (86%) create mode 100644 client/httplib/mock_test.go diff --git a/client/httplib/filter/mock/response.go b/client/httplib/http_response.go similarity index 86% rename from client/httplib/filter/mock/response.go rename to client/httplib/http_response.go index 51041ef2..89930cb1 100644 --- a/client/httplib/filter/mock/response.go +++ b/client/httplib/http_response.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package mock +package httplib import ( "bytes" @@ -21,7 +21,8 @@ import ( "net/http" ) -// it will try to convert the data to json format +// NewHttpResponseWithJsonBody will try to convert the data to json format +// usually you only use this when you want to mock http Response func NewHttpResponseWithJsonBody(data interface{}) *http.Response { var body []byte if str, ok := data.(string); ok { diff --git a/client/httplib/httplib.go b/client/httplib/httplib.go index 7c48be5e..9402eca6 100644 --- a/client/httplib/httplib.go +++ b/client/httplib/httplib.go @@ -62,6 +62,7 @@ var defaultSetting = BeegoHTTPSettings{ ReadWriteTimeout: 60 * time.Second, Gzip: true, DumpBody: true, + FilterChains: []FilterChain{mockFilter.FilterChain}, } var defaultCookieJar http.CookieJar diff --git a/client/httplib/mock.go b/client/httplib/mock.go new file mode 100644 index 00000000..691f03d2 --- /dev/null +++ b/client/httplib/mock.go @@ -0,0 +1,71 @@ +// Copyright 2020 beego +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httplib + +import ( + "context" + "net/http" + + "github.com/beego/beego/v2/core/logs" +) + +const mockCtxKey = "beego-httplib-mock" + +type Stub interface { + Mock(cond RequestCondition, resp *http.Response, err error) + Clear() + MockByPath(path string, resp *http.Response, err error) +} + +var mockFilter = &MockResponseFilter{} + +func StartMock() Stub { + return mockFilter +} + +func CtxWithMock(ctx context.Context, mock... *Mock) context.Context { + return context.WithValue(ctx, mockCtxKey, mock) +} + +func mockFromCtx(ctx context.Context) []*Mock { + ms := ctx.Value(mockCtxKey) + if ms != nil { + if res, ok := ms.([]*Mock); ok { + return res + } + logs.Error("mockCtxKey found in context, but value is not type []*Mock") + } + return nil +} + +type Mock struct { + cond RequestCondition + resp *http.Response + err error +} + +func NewMockByPath(path string, resp *http.Response, err error) *Mock { + return NewMock(NewSimpleCondition(path), resp, err) +} + +func NewMock(con RequestCondition, resp *http.Response, err error) *Mock { + return &Mock{ + cond: con, + resp: resp, + err: err, + } +} + + diff --git a/client/httplib/filter/mock/condition.go b/client/httplib/mock_condition.go similarity index 89% rename from client/httplib/filter/mock/condition.go rename to client/httplib/mock_condition.go index aade2207..5e6ff455 100644 --- a/client/httplib/filter/mock/condition.go +++ b/client/httplib/mock_condition.go @@ -12,17 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -package mock +package httplib import ( "context" "encoding/json" "net/textproto" "regexp" - - "github.com/beego/beego/v2/client/httplib" ) +type RequestCondition interface { + Match(ctx context.Context, req *BeegoHTTPRequest) bool +} + // reqCondition create condition // - path: same path // - pathReg: request path match pathReg @@ -52,7 +54,7 @@ func NewSimpleCondition(path string, opts ...simpleConditionOption) *SimpleCondi return sc } -func (sc *SimpleCondition) Match(ctx context.Context, req *httplib.BeegoHTTPRequest) bool { +func (sc *SimpleCondition) Match(ctx context.Context, req *BeegoHTTPRequest) bool { res := true if len(sc.path) > 0 { res = sc.matchPath(ctx, req) @@ -68,12 +70,12 @@ func (sc *SimpleCondition) Match(ctx context.Context, req *httplib.BeegoHTTPRequ sc.matchBodyFields(ctx, req) } -func (sc *SimpleCondition) matchPath(ctx context.Context, req *httplib.BeegoHTTPRequest) bool { +func (sc *SimpleCondition) matchPath(ctx context.Context, req *BeegoHTTPRequest) bool { path := req.GetRequest().URL.Path return path == sc.path } -func (sc *SimpleCondition) matchPathReg(ctx context.Context, req *httplib.BeegoHTTPRequest) bool { +func (sc *SimpleCondition) matchPathReg(ctx context.Context, req *BeegoHTTPRequest) bool { path := req.GetRequest().URL.Path if b, err := regexp.Match(sc.pathReg, []byte(path)); err == nil { return b @@ -81,7 +83,7 @@ func (sc *SimpleCondition) matchPathReg(ctx context.Context, req *httplib.BeegoH return false } -func (sc *SimpleCondition) matchQuery(ctx context.Context, req *httplib.BeegoHTTPRequest) bool { +func (sc *SimpleCondition) matchQuery(ctx context.Context, req *BeegoHTTPRequest) bool { qs := req.GetRequest().URL.Query() for k, v := range sc.query { if uv, ok := qs[k]; !ok || uv[0] != v { @@ -91,7 +93,7 @@ func (sc *SimpleCondition) matchQuery(ctx context.Context, req *httplib.BeegoHTT return true } -func (sc *SimpleCondition) matchHeader(ctx context.Context, req *httplib.BeegoHTTPRequest) bool { +func (sc *SimpleCondition) matchHeader(ctx context.Context, req *BeegoHTTPRequest) bool { headers := req.GetRequest().Header for k, v := range sc.header { if uv, ok := headers[k]; !ok || uv[0] != v { @@ -101,7 +103,7 @@ func (sc *SimpleCondition) matchHeader(ctx context.Context, req *httplib.BeegoHT return true } -func (sc *SimpleCondition) matchBodyFields(ctx context.Context, req *httplib.BeegoHTTPRequest) bool { +func (sc *SimpleCondition) matchBodyFields(ctx context.Context, req *BeegoHTTPRequest) bool { if len(sc.body) == 0 { return true } @@ -133,7 +135,7 @@ func (sc *SimpleCondition) matchBodyFields(ctx context.Context, req *httplib.Bee return true } -func (sc *SimpleCondition) matchMethod(ctx context.Context, req *httplib.BeegoHTTPRequest) bool { +func (sc *SimpleCondition) matchMethod(ctx context.Context, req *BeegoHTTPRequest) bool { if len(sc.method) > 0 { return sc.method == req.GetRequest().Method } diff --git a/client/httplib/filter/mock/condition_test.go b/client/httplib/mock_condition_test.go similarity index 74% rename from client/httplib/filter/mock/condition_test.go rename to client/httplib/mock_condition_test.go index 4fc6d377..643dc353 100644 --- a/client/httplib/filter/mock/condition_test.go +++ b/client/httplib/mock_condition_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package mock +package httplib import ( "context" @@ -20,7 +20,6 @@ import ( "github.com/stretchr/testify/assert" - "github.com/beego/beego/v2/client/httplib" ) func init() { @@ -29,37 +28,37 @@ func init() { func TestSimpleCondition_MatchPath(t *testing.T) { sc := NewSimpleCondition("/abc/s") - res := sc.Match(context.Background(), httplib.Get("http://localhost:8080/abc/s")) + res := sc.Match(context.Background(), Get("http://localhost:8080/abc/s")) assert.True(t, res) } func TestSimpleCondition_MatchQuery(t *testing.T) { k, v := "my-key", "my-value" sc := NewSimpleCondition("/abc/s") - res := sc.Match(context.Background(), httplib.Get("http://localhost:8080/abc/s?my-key=my-value")) + res := sc.Match(context.Background(), Get("http://localhost:8080/abc/s?my-key=my-value")) assert.True(t, res) sc = NewSimpleCondition("/abc/s", WithQuery(k, v)) - res = sc.Match(context.Background(), httplib.Get("http://localhost:8080/abc/s?my-key=my-value")) + res = sc.Match(context.Background(), Get("http://localhost:8080/abc/s?my-key=my-value")) assert.True(t, res) - res = sc.Match(context.Background(), httplib.Get("http://localhost:8080/abc/s?my-key=my-valuesss")) + res = sc.Match(context.Background(), Get("http://localhost:8080/abc/s?my-key=my-valuesss")) assert.False(t, res) - res = sc.Match(context.Background(), httplib.Get("http://localhost:8080/abc/s?my-key-a=my-value")) + res = sc.Match(context.Background(), Get("http://localhost:8080/abc/s?my-key-a=my-value")) assert.False(t, res) - res = sc.Match(context.Background(), httplib.Get("http://localhost:8080/abc/s?my-key=my-value&abc=hello")) + res = sc.Match(context.Background(), Get("http://localhost:8080/abc/s?my-key=my-value&abc=hello")) assert.True(t, res) } func TestSimpleCondition_MatchHeader(t *testing.T) { k, v := "my-header", "my-header-value" sc := NewSimpleCondition("/abc/s") - req := httplib.Get("http://localhost:8080/abc/s") + req := Get("http://localhost:8080/abc/s") assert.True(t, sc.Match(context.Background(), req)) - req = httplib.Get("http://localhost:8080/abc/s") + req = Get("http://localhost:8080/abc/s") req.Header(k, v) assert.True(t, sc.Match(context.Background(), req)) @@ -74,7 +73,7 @@ func TestSimpleCondition_MatchHeader(t *testing.T) { func TestSimpleCondition_MatchBodyField(t *testing.T) { sc := NewSimpleCondition("/abc/s") - req := httplib.Post("http://localhost:8080/abc/s") + req := Post("http://localhost:8080/abc/s") assert.True(t, sc.Match(context.Background(), req)) @@ -103,7 +102,7 @@ func TestSimpleCondition_MatchBodyField(t *testing.T) { func TestSimpleCondition_Match(t *testing.T) { sc := NewSimpleCondition("/abc/s") - req := httplib.Post("http://localhost:8080/abc/s") + req := Post("http://localhost:8080/abc/s") assert.True(t, sc.Match(context.Background(), req)) @@ -116,9 +115,9 @@ func TestSimpleCondition_Match(t *testing.T) { func TestSimpleCondition_MatchPathReg(t *testing.T) { sc := NewSimpleCondition("", WithPathReg(`\/abc\/.*`)) - req := httplib.Post("http://localhost:8080/abc/s") + req := Post("http://localhost:8080/abc/s") assert.True(t, sc.Match(context.Background(), req)) - req = httplib.Post("http://localhost:8080/abcd/s") + req = Post("http://localhost:8080/abcd/s") assert.False(t, sc.Match(context.Background(), req)) } diff --git a/client/httplib/filter/mock/filter.go b/client/httplib/mock_filter.go similarity index 65% rename from client/httplib/filter/mock/filter.go rename to client/httplib/mock_filter.go index 568aba8b..83a7b71b 100644 --- a/client/httplib/filter/mock/filter.go +++ b/client/httplib/mock_filter.go @@ -12,16 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package mock +package httplib import ( "context" + "fmt" "net/http" - - "github.com/beego/beego/v2/client/httplib" ) // MockResponse will return mock response if find any suitable mock data +// if you want to test your code using httplib, you need this. type MockResponseFilter struct { ms []*Mock } @@ -32,9 +32,14 @@ func NewMockResponseFilter() *MockResponseFilter { } } -func (m *MockResponseFilter) FilterChain(next httplib.Filter) httplib.Filter { - return func(ctx context.Context, req *httplib.BeegoHTTPRequest) (*http.Response, error) { - for _, mock := range m.ms { +func (m *MockResponseFilter) FilterChain(next Filter) Filter { + return func(ctx context.Context, req *BeegoHTTPRequest) (*http.Response, error) { + + ms := mockFromCtx(ctx) + ms = append(ms, m.ms...) + + fmt.Printf("url: %s, mock: %d \n", req.url, len(ms)) + for _, mock := range ms { if mock.cond.Match(ctx, req) { return mock.resp, mock.err } @@ -43,22 +48,16 @@ func (m *MockResponseFilter) FilterChain(next httplib.Filter) httplib.Filter { } } +func (m *MockResponseFilter) MockByPath(path string, resp *http.Response, err error) { + m.Mock(NewSimpleCondition(path), resp, err) +} + +func (m *MockResponseFilter) Clear() { + m.ms = make([]*Mock, 0, 1) +} + // Mock add mock data // If the cond.Match(...) = true, the resp and err will be returned func (m *MockResponseFilter) Mock(cond RequestCondition, resp *http.Response, err error) { - m.ms = append(m.ms, &Mock{ - cond: cond, - resp: resp, - err: err, - }) -} - -type Mock struct { - cond RequestCondition - resp *http.Response - err error -} - -type RequestCondition interface { - Match(ctx context.Context, req *httplib.BeegoHTTPRequest) bool + m.ms = append(m.ms, NewMock(cond, resp, err)) } diff --git a/client/httplib/filter/mock/filter_test.go b/client/httplib/mock_filter_test.go similarity index 86% rename from client/httplib/filter/mock/filter_test.go rename to client/httplib/mock_filter_test.go index c000b5cf..40a2185e 100644 --- a/client/httplib/filter/mock/filter_test.go +++ b/client/httplib/mock_filter_test.go @@ -12,19 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package mock +package httplib import ( "errors" "testing" "github.com/stretchr/testify/assert" - - "github.com/beego/beego/v2/client/httplib" ) func TestMockResponseFilter_FilterChain(t *testing.T) { - req := httplib.Get("http://localhost:8080/abc/s") + req := Get("http://localhost:8080/abc/s") ft := NewMockResponseFilter() expectedResp := NewHttpResponseWithJsonBody(`{}`) @@ -37,15 +35,14 @@ func TestMockResponseFilter_FilterChain(t *testing.T) { assert.Equal(t, expectedErr, err) assert.Equal(t, expectedResp, resp) - req = httplib.Get("http://localhost:8080/abcd/s") + req = Get("http://localhost:8080/abcd/s") req.AddFilters(ft.FilterChain) resp, err = req.DoRequest() assert.NotEqual(t, expectedErr, err) assert.NotEqual(t, expectedResp, resp) - - req = httplib.Get("http://localhost:8080/abc/s") + req = Get("http://localhost:8080/abc/s") req.AddFilters(ft.FilterChain) expectedResp1 := NewHttpResponseWithJsonBody(map[string]string{}) expectedErr1 := errors.New("expected error") @@ -55,7 +52,7 @@ func TestMockResponseFilter_FilterChain(t *testing.T) { assert.Equal(t, expectedErr, err) assert.Equal(t, expectedResp, resp) - req = httplib.Get("http://localhost:8080/abc/abs/bbc") + req = Get("http://localhost:8080/abc/abs/bbc") req.AddFilters(ft.FilterChain) ft.Mock(NewSimpleCondition("/abc/abs/bbc"), expectedResp1, expectedErr1) resp, err = req.DoRequest() diff --git a/client/httplib/mock_test.go b/client/httplib/mock_test.go new file mode 100644 index 00000000..1d913b29 --- /dev/null +++ b/client/httplib/mock_test.go @@ -0,0 +1,75 @@ +// Copyright 2020 beego +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httplib + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStartMock(t *testing.T) { + + defaultSetting.FilterChains = []FilterChain{mockFilter.FilterChain} + + stub := StartMock() + // defer stub.Clear() + + expectedResp := NewHttpResponseWithJsonBody([]byte(`{}`)) + expectedErr := errors.New("expected err") + + stub.Mock(NewSimpleCondition("/abc"), expectedResp, expectedErr) + + resp, err := OriginalCodeUsingHttplib() + + assert.Equal(t, expectedErr, err) + assert.Equal(t, expectedResp, resp) + +} + +// TestStartMock_Isolation Test StartMock that +// mock only work for this request +func TestStartMock_Isolation(t *testing.T) { + defaultSetting.FilterChains = []FilterChain{mockFilter.FilterChain} + // setup global stub + stub := StartMock() + globalMockResp := NewHttpResponseWithJsonBody([]byte(`{}`)) + globalMockErr := errors.New("expected err") + stub.Mock(NewSimpleCondition("/abc"), globalMockResp, globalMockErr) + + expectedResp := NewHttpResponseWithJsonBody(struct { + A string `json:"a"` + }{ + A: "aaa", + }) + expectedErr := errors.New("expected err aa") + m := NewMockByPath("/abc", expectedResp, expectedErr) + ctx := CtxWithMock(context.Background(), m) + + resp, err := OriginnalCodeUsingHttplibPassCtx(ctx) + assert.Equal(t, expectedErr, err) + assert.Equal(t, expectedResp, resp) +} + +func OriginnalCodeUsingHttplibPassCtx(ctx context.Context) (*http.Response, error) { + return Get("http://localhost:7777/abc").DoRequestWithCtx(ctx) +} + +func OriginalCodeUsingHttplib() (*http.Response, error){ + return Get("http://localhost:7777/abc").DoRequest() +}