Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
Already on GitHub? Sign in to your account
Introduces a type which can be used to mock calls #118
| @@ -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 | ||
|
|
||
| + | ||
| + // 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") | ||
| + } | ||
| +} | ||
| @@ -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 | ||
kat-co
Contributor
|
||
| + 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{} { | ||
|
|
||
| + 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
|
||
| +} | ||
| + | ||
| +// 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 { | ||
kat-co
Contributor
|
||
| + m.timesInvoked.Lock() | ||
| + defer m.timesInvoked.Unlock() | ||
| + return m.timesInvoked.value | ||
| +} | ||
| + | ||
| +func TypeAssertError(err interface{}) error { | ||
| + if err == nil { | ||
| + return nil | ||
| + } | ||
| + return err.(error) | ||
| +} | ||
Please add a comment here along the lines of: