Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow mock expectations to be ordered #1106

Merged
merged 3 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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