Skip to content

Commit

Permalink
Allow mock expectations to be ordered (#1106)
Browse files Browse the repository at this point in the history
* Allow mock expectations to be ordered

* Only say another call if it has been called before
  • Loading branch information
brackendawson authored Jun 28, 2022
1 parent 66eef0e commit cf1284f
Show file tree
Hide file tree
Showing 2 changed files with 247 additions and 14 deletions.
72 changes: 58 additions & 14 deletions mock/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
189 changes: 189 additions & 0 deletions mock/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit cf1284f

Please sign in to comment.