From 1c1bb7a43f86dc8e605889ac687092c58faac562 Mon Sep 17 00:00:00 2001 From: "andrey.suchilov" Date: Thu, 1 Dec 2022 14:06:37 +0300 Subject: [PATCH] the ability to create custom mock services --- go.mod | 1 + go.sum | 4 ++++ mocks/definition.go | 18 ++++++++--------- mocks/loader.go | 44 ++++++++++++++++++++--------------------- mocks/mocks.go | 6 +++++- mocks/reply_strategy.go | 26 ++++++++++++------------ mocks/service_mock.go | 8 ++++---- 7 files changed, 58 insertions(+), 49 deletions(-) diff --git a/go.mod b/go.mod index e53f5c0..fc8fdaa 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/kylelemons/godebug v1.1.0 github.com/lib/pq v1.3.0 github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect github.com/stretchr/testify v1.5.1 github.com/tidwall/gjson v1.6.0 gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 diff --git a/go.sum b/go.sum index db8d7f0..6d0dc98 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -27,6 +29,8 @@ github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/mocks/definition.go b/mocks/definition.go index 5fd168d..0d3fecb 100644 --- a/mocks/definition.go +++ b/mocks/definition.go @@ -7,19 +7,19 @@ import ( "sync" ) -const callsNoConstraint = -1 +const CallsNoConstraint = -1 -type definition struct { +type Definition struct { path string requestConstraints []verifier - replyStrategy replyStrategy + replyStrategy ReplyStrategy sync.Mutex calls int callsConstraint int } -func newDefinition(path string, constraints []verifier, strategy replyStrategy, callsConstraint int) *definition { - return &definition{ +func NewDefinition(path string, constraints []verifier, strategy ReplyStrategy, callsConstraint int) *Definition { + return &Definition{ path: path, requestConstraints: constraints, replyStrategy: strategy, @@ -27,7 +27,7 @@ func newDefinition(path string, constraints []verifier, strategy replyStrategy, } } -func (d *definition) Execute(w http.ResponseWriter, r *http.Request) []error { +func (d *Definition) Execute(w http.ResponseWriter, r *http.Request) []error { d.Lock() d.calls++ d.Unlock() @@ -55,7 +55,7 @@ func (d *definition) Execute(w http.ResponseWriter, r *http.Request) []error { return errors } -func (d *definition) ResetRunningContext() { +func (d *Definition) ResetRunningContext() { if s, ok := d.replyStrategy.(contextAwareStrategy); ok { s.ResetRunningContext() } @@ -64,14 +64,14 @@ func (d *definition) ResetRunningContext() { d.Unlock() } -func (d *definition) EndRunningContext() []error { +func (d *Definition) EndRunningContext() []error { d.Lock() defer d.Unlock() var errs []error if s, ok := d.replyStrategy.(contextAwareStrategy); ok { errs = s.EndRunningContext() } - if d.callsConstraint != callsNoConstraint && d.calls != d.callsConstraint { + if d.callsConstraint != CallsNoConstraint && d.calls != d.callsConstraint { err := fmt.Errorf("at path %s: number of calls does not match: expected %d, actual %d", d.path, d.callsConstraint, d.calls) errs = append(errs, err) diff --git a/mocks/loader.go b/mocks/loader.go index 3d557c3..fa02624 100644 --- a/mocks/loader.go +++ b/mocks/loader.go @@ -25,18 +25,18 @@ func (l *Loader) Load(mocksDefinition map[string]interface{}) error { } def, err := l.loadDefinition("$", definition) if err != nil { - return fmt.Errorf("unable to load definition for %s: %v", serviceName, err) + return fmt.Errorf("unable to load Definition for %s: %v", serviceName, err) } - // load the definition into the mock + // load the Definition into the mock service.SetDefinition(def) } return nil } -func (l *Loader) loadDefinition(path string, rawDef interface{}) (*definition, error) { +func (l *Loader) loadDefinition(path string, rawDef interface{}) (*Definition, error) { def, ok := rawDef.(map[interface{}]interface{}) if !ok { - return nil, fmt.Errorf("at path %s: definition must be key-values", path) + return nil, fmt.Errorf("at path %s: Definition must be key-values", path) } // load request constraints @@ -76,7 +76,7 @@ func (l *Loader) loadDefinition(path string, rawDef interface{}) (*definition, e return nil, err } - callsConstraint := callsNoConstraint + callsConstraint := CallsNoConstraint if _, ok = def["calls"]; ok { if value, ok := def["calls"].(int); ok { callsConstraint = value @@ -87,10 +87,10 @@ func (l *Loader) loadDefinition(path string, rawDef interface{}) (*definition, e return nil, err } - return newDefinition(path, requestConstraints, replyStrategy, callsConstraint), nil + return NewDefinition(path, requestConstraints, replyStrategy, callsConstraint), nil } -func (l *Loader) loadStrategy(path, strategyName string, definition map[interface{}]interface{}, ak *[]string) (replyStrategy, error) { +func (l *Loader) loadStrategy(path, strategyName string, definition map[interface{}]interface{}, ak *[]string) (ReplyStrategy, error) { switch strategyName { case "nop": return &nopReply{}, nil @@ -114,18 +114,18 @@ func (l *Loader) loadStrategy(path, strategyName string, definition map[interfac } } -func (l *Loader) loadUriVaryStrategy(path string, def map[interface{}]interface{}) (replyStrategy, error) { +func (l *Loader) loadUriVaryStrategy(path string, def map[interface{}]interface{}) (ReplyStrategy, error) { var basePath string if b, ok := def["basePath"]; ok { basePath = b.(string) } - var uris map[string]*definition + var uris map[string]*Definition if u, ok := def["uris"]; ok { urisMap, ok := u.(map[interface{}]interface{}) if !ok { return nil, errors.New("`uriVary` requires map under `uris` key") } - uris = make(map[string]*definition, len(urisMap)) + uris = make(map[string]*Definition, len(urisMap)) for uri, v := range urisMap { def, err := l.loadDefinition(path+"."+uri.(string), v) if err != nil { @@ -134,17 +134,17 @@ func (l *Loader) loadUriVaryStrategy(path string, def map[interface{}]interface{ uris[uri.(string)] = def } } - return newUriVaryReply(basePath, uris), nil + return NewUriVaryReply(basePath, uris), nil } -func (l *Loader) loadMethodVaryStrategy(path string, def map[interface{}]interface{}) (replyStrategy, error) { - var methods map[string]*definition +func (l *Loader) loadMethodVaryStrategy(path string, def map[interface{}]interface{}) (ReplyStrategy, error) { + var methods map[string]*Definition if u, ok := def["methods"]; ok { methodsMap, ok := u.(map[interface{}]interface{}) if !ok { return nil, errors.New("`methodVary` requires map under `methods` key") } - methods = make(map[string]*definition, len(methodsMap)) + methods = make(map[string]*Definition, len(methodsMap)) for method, v := range methodsMap { def, err := l.loadDefinition(path+"."+method.(string), v) if err != nil { @@ -153,10 +153,10 @@ func (l *Loader) loadMethodVaryStrategy(path string, def map[interface{}]interfa methods[method.(string)] = def } } - return newMethodVaryReply(methods), nil + return NewMethodVaryReply(methods), nil } -func (l *Loader) loadFileStrategy(path string, def map[interface{}]interface{}) (replyStrategy, error) { +func (l *Loader) loadFileStrategy(path string, def map[interface{}]interface{}) (ReplyStrategy, error) { f, ok := def["filename"] if !ok { return nil, errors.New("`file` requires `filename` key") @@ -173,10 +173,10 @@ func (l *Loader) loadFileStrategy(path string, def map[interface{}]interface{}) if err != nil { return nil, err } - return newFileReplyWithCode(filename, statusCode, headers) + return NewFileReplyWithCode(filename, statusCode, headers) } -func (l *Loader) loadConstantStrategy(path string, def map[interface{}]interface{}) (replyStrategy, error) { +func (l *Loader) loadConstantStrategy(path string, def map[interface{}]interface{}) (ReplyStrategy, error) { c, ok := def["body"] if !ok { return nil, errors.New("`constant` requires `body` key") @@ -193,10 +193,10 @@ func (l *Loader) loadConstantStrategy(path string, def map[interface{}]interface if err != nil { return nil, err } - return newConstantReplyWithCode([]byte(body), statusCode, headers), nil + return NewConstantReplyWithCode([]byte(body), statusCode, headers), nil } -func (l *Loader) loadSequenceStrategy(path string, def map[interface{}]interface{}) (replyStrategy, error) { +func (l *Loader) loadSequenceStrategy(path string, def map[interface{}]interface{}) (ReplyStrategy, error) { if _, ok := def["sequence"]; !ok { return nil, errors.New("`sequence` requires `sequence` key") } @@ -204,7 +204,7 @@ func (l *Loader) loadSequenceStrategy(path string, def map[interface{}]interface if !ok { return nil, errors.New("`sequence` must be a list") } - strategies := make([]*definition, len(seqSlice)) + strategies := make([]*Definition, len(seqSlice)) for i, v := range seqSlice { def, err := l.loadDefinition(path+"."+strconv.Itoa(i), v) if err != nil { @@ -212,7 +212,7 @@ func (l *Loader) loadSequenceStrategy(path string, def map[interface{}]interface } strategies[i] = def } - return newSequentialReply(strategies), nil + return NewSequentialReply(strategies), nil } func (l *Loader) loadHeaders(def map[interface{}]interface{}) (map[string]string, error) { diff --git a/mocks/mocks.go b/mocks/mocks.go index 6312a5d..383634f 100644 --- a/mocks/mocks.go +++ b/mocks/mocks.go @@ -24,7 +24,7 @@ func New(mocks ...*ServiceMock) *Mocks { func NewNop(serviceNames ...string) *Mocks { mocksMap := make(map[string]*ServiceMock, len(serviceNames)) for _, name := range serviceNames { - mocksMap[name] = NewServiceMock(name, newDefinition("$", nil, &failReply{}, callsNoConstraint)) + mocksMap[name] = NewServiceMock(name, NewDefinition("$", nil, &failReply{}, CallsNoConstraint)) } return &Mocks{ mocks: mocksMap, @@ -68,6 +68,10 @@ func (m *Mocks) ShutdownContext(ctx context.Context) error { return nil } +func (m *Mocks) SetMock(mock *ServiceMock) { + m.mocks[mock.ServiceName] = mock +} + func (m *Mocks) Service(serviceName string) *ServiceMock { mock, _ := m.mocks[serviceName] return mock diff --git a/mocks/reply_strategy.go b/mocks/reply_strategy.go index 5ba4c53..390373a 100644 --- a/mocks/reply_strategy.go +++ b/mocks/reply_strategy.go @@ -9,7 +9,7 @@ import ( "sync" ) -type replyStrategy interface { +type ReplyStrategy interface { HandleRequest(w http.ResponseWriter, r *http.Request) []error } @@ -19,7 +19,7 @@ type contextAwareStrategy interface { } type constantReply struct { - replyStrategy + ReplyStrategy replyBody []byte statusCode int @@ -34,7 +34,7 @@ func unhandledRequestError(r *http.Request) []error { return []error{fmt.Errorf("unhandled request to mock:\n%s", requestContent)} } -func newFileReplyWithCode(filename string, statusCode int, headers map[string]string) (replyStrategy, error) { +func NewFileReplyWithCode(filename string, statusCode int, headers map[string]string) (ReplyStrategy, error) { content, err := ioutil.ReadFile(filename) if err != nil { return nil, err @@ -47,7 +47,7 @@ func newFileReplyWithCode(filename string, statusCode int, headers map[string]st return r, nil } -func newConstantReplyWithCode(content []byte, statusCode int, headers map[string]string) replyStrategy { +func NewConstantReplyWithCode(content []byte, statusCode int, headers map[string]string) ReplyStrategy { return &constantReply{ replyBody: content, statusCode: statusCode, @@ -72,7 +72,7 @@ func (s *failReply) HandleRequest(w http.ResponseWriter, r *http.Request) []erro } type nopReply struct { - replyStrategy + ReplyStrategy } func (s *nopReply) HandleRequest(w http.ResponseWriter, r *http.Request) []error { @@ -81,14 +81,14 @@ func (s *nopReply) HandleRequest(w http.ResponseWriter, r *http.Request) []error } type uriVaryReply struct { - replyStrategy + ReplyStrategy contextAwareStrategy basePath string - variants map[string]*definition + variants map[string]*Definition } -func newUriVaryReply(basePath string, variants map[string]*definition) replyStrategy { +func NewUriVaryReply(basePath string, variants map[string]*Definition) ReplyStrategy { return &uriVaryReply{ basePath: strings.TrimRight(basePath, "/") + "/", variants: variants, @@ -120,13 +120,13 @@ func (s *uriVaryReply) EndRunningContext() []error { } type methodVaryReply struct { - replyStrategy + ReplyStrategy contextAwareStrategy - variants map[string]*definition + variants map[string]*Definition } -func newMethodVaryReply(variants map[string]*definition) replyStrategy { +func NewMethodVaryReply(variants map[string]*Definition) ReplyStrategy { return &methodVaryReply{ variants: variants, } @@ -155,7 +155,7 @@ func (s *methodVaryReply) EndRunningContext() []error { return errs } -func newSequentialReply(strategies []*definition) replyStrategy { +func NewSequentialReply(strategies []*Definition) ReplyStrategy { return &sequentialReply{ sequence: strategies, } @@ -164,7 +164,7 @@ func newSequentialReply(strategies []*definition) replyStrategy { type sequentialReply struct { sync.Mutex count int - sequence []*definition + sequence []*Definition } func (s *sequentialReply) ResetRunningContext() { diff --git a/mocks/service_mock.go b/mocks/service_mock.go index ec5f7f6..3394b83 100644 --- a/mocks/service_mock.go +++ b/mocks/service_mock.go @@ -10,15 +10,15 @@ import ( type ServiceMock struct { server *http.Server listener net.Listener - mock *definition - defaultDefinition *definition + mock *Definition + defaultDefinition *Definition sync.Mutex errors []error ServiceName string } -func NewServiceMock(serviceName string, mock *definition) *ServiceMock { +func NewServiceMock(serviceName string, mock *Definition) *ServiceMock { return &ServiceMock{ mock: mock, defaultDefinition: mock, @@ -62,7 +62,7 @@ func (m *ServiceMock) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -func (m *ServiceMock) SetDefinition(newDefinition *definition) { +func (m *ServiceMock) SetDefinition(newDefinition *Definition) { m.Lock() defer m.Unlock() m.mock = newDefinition