Skip to content

Commit

Permalink
add spy support for mock lib
Browse files Browse the repository at this point in the history
  • Loading branch information
ysmood committed Nov 2, 2023
1 parent 8f3e345 commit e2a14e1
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 130 deletions.
5 changes: 5 additions & 0 deletions lib/example/05_mocking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
133 changes: 3 additions & 130 deletions lib/mock/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@
package mock

import (
"fmt"
"reflect"
"regexp"
"runtime"
"strings"
"sync"

"github.com/ysmood/got/lib/utils"
)

// Fallbackable interface
Expand All @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions lib/mock/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

{
Expand Down
50 changes: 50 additions & 0 deletions lib/mock/spy.go
Original file line number Diff line number Diff line change
@@ -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
}
137 changes: 137 additions & 0 deletions lib/mock/stub.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit e2a14e1

Please sign in to comment.