From cf1284f8dd6f0bf34eed4ab8808ef88f40f7d00f Mon Sep 17 00:00:00 2001 From: Bracken Date: Tue, 28 Jun 2022 13:13:45 +0100 Subject: [PATCH] Allow mock expectations to be ordered (#1106) * Allow mock expectations to be ordered * Only say another call if it has been called before --- mock/mock.go | 72 ++++++++++++++---- mock/mock_test.go | 189 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 14 deletions(-) diff --git a/mock/mock.go b/mock/mock.go index 3833cb6f0..f0af8246c 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -70,6 +70,9 @@ type Call struct { // if the PanicMsg is set to a non nil string the function call will panic // irrespective of other settings PanicMsg *string + + // Calls which must be satisfied before this call can be + requires []*Call } func newCall(parent *Mock, methodName string, callerInfo []string, methodArguments ...interface{}) *Call { @@ -236,6 +239,27 @@ func (c *Call) Unset() *Call { return c } +// NotBefore indicates that the mock should only be called after the referenced +// calls have been called as expected. The referenced calls may be from the +// same mock instance and/or other mock instances. +// +// Mock.On("Do").Return(nil).Notbefore( +// Mock.On("Init").Return(nil) +// ) +func (c *Call) NotBefore(calls ...*Call) *Call { + c.lock() + defer c.unlock() + + for _, call := range calls { + if call.Parent == nil { + panic("not before calls must be created with Mock.On()") + } + } + + c.requires = append(c.requires, calls...) + return c +} + // Mock is the workhorse used to track activity on another object. // For an example of its usage, refer to the "Example Usage" section at the top // of this document. @@ -462,6 +486,25 @@ func (m *Mock) MethodCalled(methodName string, arguments ...interface{}) Argumen } } + for _, requirement := range call.requires { + if satisfied, _ := requirement.Parent.checkExpectation(requirement); !satisfied { + m.mutex.Unlock() + m.fail("mock: Unexpected Method Call\n-----------------------------\n\n%s\n\nMust not be called before%s:\n\n%s", + callString(call.Method, call.Arguments, true), + func() (s string) { + if requirement.totalCalls > 0 { + s = " another call of" + } + if call.Parent != requirement.Parent { + s += " method from another mock instance" + } + return + }(), + callString(requirement.Method, requirement.Arguments, true), + ) + } + } + if call.Repeatability == 1 { call.Repeatability = -1 } else if call.Repeatability > 1 { @@ -541,32 +584,33 @@ func (m *Mock) AssertExpectations(t TestingT) bool { m.mutex.Lock() defer m.mutex.Unlock() - var somethingMissing bool var failedExpectations int // iterate through each expectation expectedCalls := m.expectedCalls() for _, expectedCall := range expectedCalls { - if !expectedCall.optional && !m.methodWasCalled(expectedCall.Method, expectedCall.Arguments) && expectedCall.totalCalls == 0 { - somethingMissing = true + satisfied, reason := m.checkExpectation(expectedCall) + if !satisfied { failedExpectations++ - t.Logf("FAIL:\t%s(%s)\n\t\tat: %s", expectedCall.Method, expectedCall.Arguments.String(), expectedCall.callerInfo) - } else { - if expectedCall.Repeatability > 0 { - somethingMissing = true - failedExpectations++ - t.Logf("FAIL:\t%s(%s)\n\t\tat: %s", expectedCall.Method, expectedCall.Arguments.String(), expectedCall.callerInfo) - } else { - t.Logf("PASS:\t%s(%s)", expectedCall.Method, expectedCall.Arguments.String()) - } } + t.Logf(reason) } - if somethingMissing { + if failedExpectations != 0 { t.Errorf("FAIL: %d out of %d expectation(s) were met.\n\tThe code you are testing needs to make %d more call(s).\n\tat: %s", len(expectedCalls)-failedExpectations, len(expectedCalls), failedExpectations, assert.CallerInfo()) } - return !somethingMissing + return failedExpectations == 0 +} + +func (m *Mock) checkExpectation(call *Call) (bool, string) { + if !call.optional && !m.methodWasCalled(call.Method, call.Arguments) && call.totalCalls == 0 { + return false, fmt.Sprintf("FAIL:\t%s(%s)\n\t\tat: %s", call.Method, call.Arguments.String(), call.callerInfo) + } + if call.Repeatability > 0 { + return false, fmt.Sprintf("FAIL:\t%s(%s)\n\t\tat: %s", call.Method, call.Arguments.String(), call.callerInfo) + } + return true, fmt.Sprintf("PASS:\t%s(%s)", call.Method, call.Arguments.String()) } // AssertNumberOfCalls asserts that the method was called expectedCalls times. diff --git a/mock/mock_test.go b/mock/mock_test.go index 06b25012e..5cb40e366 100644 --- a/mock/mock_test.go +++ b/mock/mock_test.go @@ -820,6 +820,195 @@ func Test_Mock_Return_Nothing(t *testing.T) { assert.Equal(t, 0, len(call.ReturnArguments)) } +func Test_Mock_Return_NotBefore_In_Order(t *testing.T) { + var mockedService = new(TestExampleImplementation) + + b := mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil) + c := mockedService. + On("TheExampleMethod2", true). + Return(). + NotBefore(b) + + require.Equal(t, []*Call{b, c}, mockedService.ExpectedCalls) + require.NotPanics(t, func() { + mockedService.TheExampleMethod(1, 2, 3) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod2(true) + }) +} + +func Test_Mock_Return_NotBefore_Out_Of_Order(t *testing.T) { + var mockedService = new(TestExampleImplementation) + + b := mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil).Twice() + c := mockedService. + On("TheExampleMethod2", true). + Return(). + NotBefore(b) + + require.Equal(t, []*Call{b, c}, mockedService.ExpectedCalls) + + expectedPanicString := `mock: Unexpected Method Call +----------------------------- + +TheExampleMethod2(bool) + 0: true + +Must not be called before: + +TheExampleMethod(int,int,int) + 0: 1 + 1: 2 + 2: 3` + require.PanicsWithValue(t, expectedPanicString, func() { + mockedService.TheExampleMethod2(true) + }) +} + +func Test_Mock_Return_NotBefore_Not_Enough_Times(t *testing.T) { + var mockedService = new(TestExampleImplementation) + + b := mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil).Twice() + c := mockedService. + On("TheExampleMethod2", true). + Return(). + NotBefore(b) + + require.Equal(t, []*Call{b, c}, mockedService.ExpectedCalls) + + require.NotPanics(t, func() { + mockedService.TheExampleMethod(1, 2, 3) + }) + expectedPanicString := `mock: Unexpected Method Call +----------------------------- + +TheExampleMethod2(bool) + 0: true + +Must not be called before another call of: + +TheExampleMethod(int,int,int) + 0: 1 + 1: 2 + 2: 3` + require.PanicsWithValue(t, expectedPanicString, func() { + mockedService.TheExampleMethod2(true) + }) +} + +func Test_Mock_Return_NotBefore_Different_Mock_In_Order(t *testing.T) { + var ( + mockedService1 = new(TestExampleImplementation) + mockedService2 = new(TestExampleImplementation) + ) + + b := mockedService1. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil) + c := mockedService2. + On("TheExampleMethod2", true). + Return(). + NotBefore(b) + + require.Equal(t, []*Call{c}, mockedService2.ExpectedCalls) + require.NotPanics(t, func() { + mockedService1.TheExampleMethod(1, 2, 3) + }) + require.NotPanics(t, func() { + mockedService2.TheExampleMethod2(true) + }) +} +func Test_Mock_Return_NotBefore_Different_Mock_Out_Of_Order(t *testing.T) { + var ( + mockedService1 = new(TestExampleImplementation) + mockedService2 = new(TestExampleImplementation) + ) + + b := mockedService1. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil) + c := mockedService2. + On("TheExampleMethod2", true). + Return(). + NotBefore(b) + + require.Equal(t, []*Call{c}, mockedService2.ExpectedCalls) + + expectedPanicString := `mock: Unexpected Method Call +----------------------------- + +TheExampleMethod2(bool) + 0: true + +Must not be called before method from another mock instance: + +TheExampleMethod(int,int,int) + 0: 1 + 1: 2 + 2: 3` + require.PanicsWithValue(t, expectedPanicString, func() { + mockedService2.TheExampleMethod2(true) + }) +} + +func Test_Mock_Return_NotBefore_In_Order_With_Non_Dependant(t *testing.T) { + var mockedService = new(TestExampleImplementation) + + a := mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil) + b := mockedService. + On("TheExampleMethod", 4, 5, 6). + Return(4, nil) + c := mockedService. + On("TheExampleMethod2", true). + Return(). + NotBefore(a, b) + d := mockedService. + On("TheExampleMethod7", []bool{}).Return(nil) + + require.Equal(t, []*Call{a, b, c, d}, mockedService.ExpectedCalls) + require.NotPanics(t, func() { + mockedService.TheExampleMethod7([]bool{}) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod(1, 2, 3) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod7([]bool{}) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod(4, 5, 6) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod7([]bool{}) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod2(true) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod7([]bool{}) + }) +} + +func Test_Mock_Return_NotBefore_Orphan_Call(t *testing.T) { + var mockedService = new(TestExampleImplementation) + + require.PanicsWithValue(t, "not before calls must be created with Mock.On()", func() { + mockedService. + On("TheExampleMethod2", true). + Return(). + NotBefore(&Call{Method: "Not", Arguments: Arguments{"how", "it's"}, ReturnArguments: Arguments{"done"}}) + }) +} + func Test_Mock_findExpectedCall(t *testing.T) { m := new(Mock)