Introduces a type which can be used to mock calls #118

Merged
merged 2 commits into from Nov 24, 2016
Jump to file or symbol
Failed to load files and symbols.
+190 −0
Split
View
@@ -0,0 +1,82 @@
+package testing_test
+
+import (
+ "fmt"
+
+ "log"
+
+ "github.com/juju/loggo"
+ "github.com/juju/testing"
+)
+
+type ExampleInterfaceToMock interface {
+ Add(a, b int) int
+ Div(a, b int) (int, error)
+}
+
+type fakeType struct {
+ ExampleInterfaceToMock
+ *testing.CallMocker
+}
+
+func (f *fakeType) Add(a, b int) int {
+ results := f.MethodCall(f, "Add", a, b)
+ return results[0].(int)
+}
+
+func (f *fakeType) Div(a, b int) (int, error) {
+ results := f.MethodCall(f, "Div", a, b)
+ return results[0].(int), testing.TypeAssertError(results[1])
+}
+
+type ExampleTypeWhichUsesInterface struct {
+ calculator ExampleInterfaceToMock
+}
+
+func (e *ExampleTypeWhichUsesInterface) Add(nums ...int) int {
+ var tally int
+ for n := range nums {
+ tally = e.calculator.Add(tally, n)
+ }
+ return tally
+}
+
+func (e *ExampleTypeWhichUsesInterface) Div(nums ...int) (int, error) {
+ var tally int
+ var err error
+ for n := range nums {
+ tally, err = e.calculator.Div(tally, n)
+ if err != nil {
+ break
+ }
+ }
+ return tally, err
+}
+
+func Example() {
+ var logger loggo.Logger
@mjs

mjs Nov 23, 2016

Please add a comment here along the lines of:

// Set up mock
@kat-co

kat-co Nov 24, 2016

Contributor

Done

+
+ // Set a fake type which mocks out calls.
+ mock := &fakeType{CallMocker: testing.NewCallMocker(logger)}
+ mock.Call("Add", 1, 1).Returns(2)
+ mock.Call("Div", 1, 1).Returns(1, nil)
+ mock.Call("Div", 1, 0).Returns(0, fmt.Errorf("cannot divide by zero"))
+
+ // Pass in the mock which satisifes a dependency of
+ // ExampleTypeWhichUsesInterface. This allows us to inject mocked
+ // calls.
+ example := ExampleTypeWhichUsesInterface{calculator: mock}
+ if example.Add(1, 1) != 2 {
+ log.Fatal("unexpected result")
+ }
+
+ if result, err := example.Div(1, 1); err != nil {
+ log.Fatalf("unexpected error: %v", err)
+ } else if result != 1 {
+ log.Fatal("unexpected result")
+ }
+
+ if _, err := example.Div(1, 0); err == nil {
+ log.Fatal("did not receive expected divide by zero error")
+ }
+}
View
108 mocker.go
@@ -0,0 +1,108 @@
+package testing
+
+import (
+ "reflect"
+ "sync"
+
+ "github.com/juju/loggo"
+)
+
+// NewCallMocker returns a CallMocker which will log calls and results
+// utilizing the given logger.
+func NewCallMocker(logger loggo.Logger) *CallMocker {
+ return &CallMocker{
+ logger: logger,
+ results: make(map[string][]*callMockReturner),
+ }
+}
+
+// CallMocker is a tool which allows tests to dynamically specify
+// results for a given set of input parameters.
+type CallMocker struct {
+ Stub
+
+ logger loggo.Logger
@mjs

mjs Nov 23, 2016

What if this took a gc.C and used the Log method on that?

@kat-co

kat-co Nov 24, 2016

Contributor

It seems like it would unnecessarily couple this type to gocheck. I wish gc.C satisified some kind of logger interface so that we could just pass it in.

@mjs

mjs Nov 24, 2016

Yeah, that would be better... Not coupling this to gc.C makes sense I guess, but the way it is ties this to loggo which isn't much better. Maybe at least make this a loggo style interface?

+ results map[string][]*callMockReturner
+}
+
+// MethodCall logs the call to a method and any results that will be
+// returned. It returns the results previously specified by the Call
+// function. If no results were specified, the returned slice will be
+// nil.
+func (m *CallMocker) MethodCall(receiver interface{}, fnName string, args ...interface{}) []interface{} {
@mjs

mjs Nov 23, 2016

What about naming this RecordCall? That would be closer to what it does.

+ m.Stub.MethodCall(receiver, fnName, args...)
+ m.logger.Debugf("Call: %s(%v)", fnName, args)
+ results := m.Results(fnName, args...)
+ m.logger.Debugf("Results: %v", results)
+ return results
+}
+
+// Results returns any results previously specified by calls to the
+// Call method. If there are no results, the returned slice will be
+// nil.
+func (m *CallMocker) Results(fnName string, args ...interface{}) []interface{} {
+ for _, r := range m.results[fnName] {
+ if reflect.DeepEqual(r.args, args) == false {
+ continue
+ }
+ r.logCall()
+ return r.retVals
+ }
+ return nil
@mjs

mjs Nov 23, 2016

I wonder if this should be an error, that is, shouldn't the test author be required to specify each possible call up front? Otherwise tests could end up passing unintentionally.

@kat-co

kat-co Nov 24, 2016

Contributor

I thought about panicing here; what happens in practice is that when people try to resolve an index on this result, they get a panic then indicating that they haven't specified a necessary call/result. Even if the function/method only returns one result, they'll have to do results[0].(int) (or whatever), so there is no risk that tests will pass unintentionally.

What do you think?

@mjs

mjs Nov 24, 2016

I'm ok with that.

+}
+
+// Call is the first half a chained-predicate which registers that
+// calls to a function named fnName with arguments args should return
+// some value. The returned values are handled by the returned type,
+// callMockReturner.
+func (m *CallMocker) Call(fnName string, args ...interface{}) *callMockReturner {
+ returner := &callMockReturner{args: args}
+ // Push on the front to hide old results.
+ m.results[fnName] = append([]*callMockReturner{returner}, m.results[fnName]...)
+ return returner
+}
+
+type callMockReturner struct {
+ // args holds a reference to the arguments for which the retVals
+ // are valid.
+ args []interface{}
+
+ // retVals holds a reference to the values that should be returned
+ // when the values held by args are seen.
+ retVals []interface{}
+
+ // timesInvoked records the number of times this return has been
+ // reached.
+ timesInvoked struct {
+ sync.Mutex
+
+ value int
+ }
+}
+
+// Returns declares that this returner should return retVals when
+// called. It returns a closure which can be called to determine the
+// number of times this return has happened.
+func (m *callMockReturner) Returns(retVals ...interface{}) func() int {
+ m.retVals = retVals
+ return m.numTimesInvoked
+}
+
+func (m *callMockReturner) logCall() {
+ m.timesInvoked.Lock()
+ defer m.timesInvoked.Unlock()
+ m.timesInvoked.value++
+}
+
+func (m *callMockReturner) numTimesInvoked() int {
@mjs

mjs Nov 23, 2016

Although this seems useful, it's not exposed or used

@kat-co

kat-co Nov 24, 2016

Contributor

It's returned from Returns so that you can check that functions are called. Example here.

@mjs

mjs Nov 24, 2016

Duh... ignore me.

@kat-co

kat-co Nov 24, 2016

Contributor

Very easy to miss!

+ m.timesInvoked.Lock()
+ defer m.timesInvoked.Unlock()
+ return m.timesInvoked.value
+}
+
+func TypeAssertError(err interface{}) error {
+ if err == nil {
+ return nil
+ }
+ return err.(error)
+}