diff --git a/lib/example/05_mocking_test.go b/lib/example/05_mocking_test.go index f177611..3157a2e 100644 --- a/lib/example/05_mocking_test.go +++ b/lib/example/05_mocking_test.go @@ -47,6 +47,11 @@ func TestMocking(t *testing.T) { mock.On(m, m.Write).When([]byte("3")).Return(1, nil) example.ServeSum(m, &http.Request{URL: u}) + + // We can use the Calls helper to get all the input and output history of a method. + // Check the lib/example/.got/snapshots/TestMocking/calls.gop file for the details. + g.Snapshot("calls", m.Calls(m.Write)) + g.Desc("the Write should be called twice").Len(m.Calls(m.Write), 2) } // mock the rand.Source diff --git a/lib/mock/mock.go b/lib/mock/mock.go index 8426fe4..e475026 100644 --- a/lib/mock/mock.go +++ b/lib/mock/mock.go @@ -2,14 +2,11 @@ package mock import ( - "fmt" "reflect" "regexp" "runtime" "strings" "sync" - - "github.com/ysmood/got/lib/utils" ) // Fallbackable interface @@ -24,6 +21,8 @@ type Mock struct { fallback reflect.Value stubs map[string]interface{} + + calls map[string][]Call } // Fallback the methods that are not stubbed to fb. @@ -34,22 +33,6 @@ func (m *Mock) Fallback(fb interface{}) { m.fallback = reflect.ValueOf(fb) } -// Stub the method with stub -func Stub[M any](mock Fallbackable, method M, stub M) { - panicIfNotFunc(method) - - m := toMock(mock) - - m.lock.Lock() - defer m.lock.Unlock() - - if m.stubs == nil { - m.stubs = map[string]interface{}{} - } - - m.stubs[fnName(method)] = stub -} - // Stop the stub func (m *Mock) Stop(method any) { panicIfNotFunc(method) @@ -84,117 +67,7 @@ func Proxy[M any](mock Fallbackable, method M) M { panic(m.fallback.Type().String() + " doesn't have method: " + name) } - return m.fallback.MethodByName(name).Interface().(M) -} - -// StubOn utils -type StubOn struct { - when []*StubWhen -} - -// StubWhen utils -type StubWhen struct { - lock *sync.Mutex - on *StubOn - in []interface{} - ret *StubReturn - count int // how many times this stub has been matched -} - -// StubReturn utils -type StubReturn struct { - on *StubOn - out []reflect.Value - times *StubTimes -} - -// StubTimes utils -type StubTimes struct { - count int -} - -// On helper to stub methods to conditionally return values. -func On[M any](mock Fallbackable, method M) *StubOn { - panicIfNotFunc(method) - - m := toMock(mock) - - s := &StubOn{ - when: []*StubWhen{}, - } - - eq := func(in, arg []interface{}) bool { - for i := 0; i < len(in); i++ { - if in[i] != Any && utils.Compare(in[i], arg[i]) != 0 { - return false - } - } - return true - } - - t := reflect.TypeOf(method) - - fn := reflect.MakeFunc(t, func(args []reflect.Value) []reflect.Value { - argsIt := utils.ToInterfaces(args) - for _, when := range s.when { - if eq(when.in, argsIt) { - when.lock.Lock() - - when.ret.times.count-- - if when.ret.times.count == 0 { - m.Stop(method) - } - - when.count++ - - when.lock.Unlock() - - return toReturnValues(t, when.ret.out) - } - } - panic(fmt.Sprintf("No mock.StubOn.When matches: %#v", argsIt)) - }) - - Stub(m, method, fn.Interface().(M)) - - return s -} - -// Any input -var Any = struct{}{} - -// When input args of stubbed method matches in -func (s *StubOn) When(in ...interface{}) *StubWhen { - w := &StubWhen{lock: &sync.Mutex{}, on: s, in: in} - s.when = append(s.when, w) - return w -} - -// Return the out as the return values of stubbed method -func (s *StubWhen) Return(out ...interface{}) *StubReturn { - r := &StubReturn{on: s.on, out: utils.ToValues(out)} - r.Times(0) - s.ret = r - return r -} - -// Count returns how many times this condition has been matched -func (s *StubWhen) Count() int { - s.lock.Lock() - defer s.lock.Unlock() - return s.count -} - -// Times specifies how how many stubs before stop, if n <= 0 it will never stop. -func (s *StubReturn) Times(n int) *StubOn { - t := &StubTimes{count: n} - s.times = t - return s.on -} - -// Once specifies stubs only once before stop -func (s *StubReturn) Once() *StubOn { - return s.Times(1) + return m.spy(name, m.fallback.MethodByName(name).Interface()).(M) } func toMock(mock Fallbackable) *Mock { diff --git a/lib/mock/mock_test.go b/lib/mock/mock_test.go index e68e513..2e904b7 100644 --- a/lib/mock/mock_test.go +++ b/lib/mock/mock_test.go @@ -83,6 +83,8 @@ func TestMockUtils(t *testing.T) { g.Eq(n, 0) g.Eq(when.Count(), 2) + g.Len(m.Calls(m.Write), 3) + g.Snapshot("calls", m.Calls(m.Write)) } { diff --git a/lib/mock/spy.go b/lib/mock/spy.go new file mode 100644 index 0000000..875404e --- /dev/null +++ b/lib/mock/spy.go @@ -0,0 +1,50 @@ +package mock + +import "reflect" + +// Call record the input and output of a method call +type Call struct { + Input []any + Return []any +} + +// Calls returns all the calls of method +func (m *Mock) Calls(method any) []Call { + panicIfNotFunc(method) + + m.lock.Lock() + defer m.lock.Unlock() + + return m.calls[fnName(method)] +} + +// Record all the input and output of a method +func (m *Mock) spy(name string, fn any) any { + v := reflect.ValueOf(fn) + t := v.Type() + + if m.calls == nil { + m.calls = map[string][]Call{} + } + + return reflect.MakeFunc(t, func(args []reflect.Value) []reflect.Value { + ret := v.Call(args) + + m.lock.Lock() + m.calls[name] = append(m.calls[name], Call{ + valToInterface(args), + valToInterface(ret), + }) + m.lock.Unlock() + + return ret + }).Interface() +} + +func valToInterface(list []reflect.Value) []any { + ret := make([]any, len(list)) + for i, v := range list { + ret[i] = v.Interface() + } + return ret +} diff --git a/lib/mock/stub.go b/lib/mock/stub.go new file mode 100644 index 0000000..454e741 --- /dev/null +++ b/lib/mock/stub.go @@ -0,0 +1,137 @@ +package mock + +import ( + "fmt" + "reflect" + "sync" + + "github.com/ysmood/got/lib/utils" +) + +// Stub the method with stub +func Stub[M any](mock Fallbackable, method M, stub M) { + panicIfNotFunc(method) + + m := toMock(mock) + + m.lock.Lock() + defer m.lock.Unlock() + + if m.stubs == nil { + m.stubs = map[string]interface{}{} + } + + name := fnName(method) + + m.stubs[name] = m.spy(name, stub) +} + +// StubOn utils +type StubOn struct { + when []*StubWhen +} + +// StubWhen utils +type StubWhen struct { + lock *sync.Mutex + on *StubOn + in []interface{} + ret *StubReturn + count int // how many times this stub has been matched +} + +// StubReturn utils +type StubReturn struct { + on *StubOn + out []reflect.Value + times *StubTimes +} + +// StubTimes utils +type StubTimes struct { + count int +} + +// On helper to stub methods to conditionally return values. +func On[M any](mock Fallbackable, method M) *StubOn { + panicIfNotFunc(method) + + m := toMock(mock) + + s := &StubOn{ + when: []*StubWhen{}, + } + + eq := func(in, arg []interface{}) bool { + for i := 0; i < len(in); i++ { + if in[i] != Any && utils.Compare(in[i], arg[i]) != 0 { + return false + } + } + return true + } + + t := reflect.TypeOf(method) + + fn := reflect.MakeFunc(t, func(args []reflect.Value) []reflect.Value { + argsIt := utils.ToInterfaces(args) + for _, when := range s.when { + if eq(when.in, argsIt) { + when.lock.Lock() + + when.ret.times.count-- + if when.ret.times.count == 0 { + m.Stop(method) + } + + when.count++ + + when.lock.Unlock() + + return toReturnValues(t, when.ret.out) + } + } + panic(fmt.Sprintf("No mock.StubOn.When matches: %#v", argsIt)) + }) + + Stub(m, method, fn.Interface().(M)) + + return s +} + +// Any input +var Any = struct{}{} + +// When input args of stubbed method matches in +func (s *StubOn) When(in ...interface{}) *StubWhen { + w := &StubWhen{lock: &sync.Mutex{}, on: s, in: in} + s.when = append(s.when, w) + return w +} + +// Return the out as the return values of stubbed method +func (s *StubWhen) Return(out ...interface{}) *StubReturn { + r := &StubReturn{on: s.on, out: utils.ToValues(out)} + r.Times(0) + s.ret = r + return r +} + +// Count returns how many times this condition has been matched +func (s *StubWhen) Count() int { + s.lock.Lock() + defer s.lock.Unlock() + return s.count +} + +// Times specifies how how many stubs before stop, if n <= 0 it will never stop. +func (s *StubReturn) Times(n int) *StubOn { + t := &StubTimes{count: n} + s.times = t + return s.on +} + +// Once specifies stubs only once before stop +func (s *StubReturn) Once() *StubOn { + return s.Times(1) +}