From 3631f9c79b33a6cb2b19cec0e92f80f2f2cc8c38 Mon Sep 17 00:00:00 2001 From: Cameron Little Date: Sun, 30 May 2021 18:13:37 +0200 Subject: [PATCH] Add `AssertExpectationsInOrder` This adds a function to assert mock calls in order, resolving #741. I've added all the test cases I can think of and tried to design the edge cases around repeatability and optionality to make sense with the rest of the package. My one concern is about the v2 work - I hope this can fit into v1 with minimal pain on the maintainers side. > AssertExpectationsInOrder asserts that everything specified with On and Return was in fact called as expected in the order expected. Expectations set up for a specific number of times must be called that number of times before the next call to the mock is made. If optional, they do not need to be called the full number of times. Expectation with no specific limit must be called at least once unless optional. --- README.md | 5 + mock/mock.go | 141 ++++++++++++++++++++ mock/mock_test.go | 323 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 463 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c78250e41..56689aad1 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,8 @@ func TestSomething(t *testing.T) { // assert that the expectations were met testObj.AssertExpectations(t) + // assert that the expectations were met in order + testObj.AssertExpectationsInOrder(t) } @@ -188,6 +190,9 @@ func TestSomethingWithPlaceholder(t *testing.T) { // assert that the expectations were met testObj.AssertExpectations(t) + // assert that the expectations were met in order + testObj.AssertExpectationsInOrder(t) + } ``` diff --git a/mock/mock.go b/mock/mock.go index 5d445c6d3..3d0d8c234 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -533,6 +533,137 @@ func (m *Mock) AssertExpectations(t TestingT) bool { return !somethingMissing } +// AssertExpectationsInOrder asserts that everything specified with On and Return +// was in fact called as expected in the order expected. Expectations set up for +// a specific number of times must be called that number of times before the next +// call to the mock is made. If optional, they do not need to be called the full +// number of times. Expectation with no specific limit must be called at least +// once unless optional. +func (m *Mock) AssertExpectationsInOrder(t TestingT) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + m.mutex.Lock() + defer m.mutex.Unlock() + var somethingMissing bool + var failedExpectations int + + expectedCalls := m.expectedCalls() + actualCalls := m.calls() + + actualCallPtr := 0 + + // iterate through each expectation + for _, expectedCall := range expectedCalls { + if expectedCall.Repeatability == 0 { + // if the number of calls is unbounded, ensure the call was made at least once as expected + wasCalled := false + + for { + if actualCallPtr >= len(actualCalls) { + // end of actual call list, this is a problem only if call is required an not yet called + if expectedCall.totalCalls == 0 && !expectedCall.optional { + somethingMissing = true + failedExpectations++ + t.Logf( + "FAIL:\texpected %s(%s) to be called\n\t\tat: %s", + expectedCall.Method, + expectedCall.Arguments.String(), + expectedCall.callerInfo, + ) + } + break + } + + if !m.callsMatch(expectedCall, &actualCalls[actualCallPtr]) { + break + } + + wasCalled = true + actualCallPtr++ + } + + if !wasCalled && !expectedCall.optional { + somethingMissing = true + failedExpectations++ + t.Logf( + "FAIL:\texpected %s(%s)\n\t\tat: %s", + expectedCall.Method, + expectedCall.Arguments.String(), + expectedCall.callerInfo, + ) + } + } else { + // if there's a specific number of calls expected, check each one + + if expectedCall.Repeatability != -1 { + if !expectedCall.optional { + somethingMissing = true + failedExpectations++ + expectedCallsStr := "1 time" + if expectedCall.Repeatability+expectedCall.totalCalls != 1 { + expectedCallsStr = fmt.Sprintf("%d times", expectedCall.Repeatability+expectedCall.totalCalls) + } + actualCallsStr := "1 time" + if expectedCall.totalCalls != 1 { + actualCallsStr = fmt.Sprintf("%d times", expectedCall.totalCalls) + } + t.Logf( + "FAIL:\texpected %s(%s) to be called %s, actually called %s\n\t\tat: %s", + expectedCall.Method, + expectedCall.Arguments.String(), + expectedCallsStr, + actualCallsStr, + expectedCall.callerInfo, + ) + } + break + } + + for i := 0; i < expectedCall.totalCalls; i++ { + if actualCallPtr >= len(actualCalls) { + // this should never happen because `.Times` prevents additional calls + // before this point can be hit + somethingMissing = true + failedExpectations++ + t.Logf( + "FAIL:\texpected %s(%s) to be called\n\t\tat: %s", + expectedCall.Method, + expectedCall.Arguments.String(), + expectedCall.callerInfo, + ) + break + } + + actualCall := actualCalls[actualCallPtr] + + wasCalled := m.callsMatch(expectedCall, &actualCall) + + if !wasCalled && !expectedCall.optional { + somethingMissing = true + failedExpectations++ + t.Logf( + "FAIL:\texpected %s(%s)\nactual %s(%s)\n\t\tat: %s", + expectedCall.Method, + expectedCall.Arguments.String(), + actualCall.Method, + actualCall.Arguments.String(), + expectedCall.callerInfo, + ) + } + + actualCallPtr++ + } + } + } + + if somethingMissing { + 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 +} + // AssertNumberOfCalls asserts that the method was called expectedCalls times. func (m *Mock) AssertNumberOfCalls(t TestingT, methodName string, expectedCalls int) bool { if h, ok := t.(tHelper); ok { @@ -643,6 +774,16 @@ func (m *Mock) methodWasCalled(methodName string, expected []interface{}) bool { return false } +func (m *Mock) callsMatch(expected, actual *Call) bool { + if actual.Method == expected.Method { + _, differences := Arguments(expected.Arguments).Diff(actual.Arguments) + if differences == 0 { + return true + } + } + return false +} + func (m *Mock) expectedCalls() []*Call { return append([]*Call{}, m.ExpectedCalls...) } diff --git a/mock/mock_test.go b/mock/mock_test.go index 097addc8a..36525dd13 100644 --- a/mock/mock_test.go +++ b/mock/mock_test.go @@ -1065,6 +1065,317 @@ func Test_Mock_AssertExpectations_With_Repeatability(t *testing.T) { } +func Test_Mock_AssertExpectationsInOrder_InOrder(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_InOrder", 1, 2, 3).Return() + mockedService.On("Test_Mock_AssertExpectationsInOrder_InOrder", 2, 3, 4).Return() + mockedService.On("Test_Mock_AssertExpectationsInOrder_InOrder", 3, 4, 5).Return() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make most of the calls + mockedService.Called(1, 2, 3) + mockedService.Called(2, 3, 4) + + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the last call + mockedService.Called(3, 4, 5) + + // now assert expectations + assert.True(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +func Test_Mock_AssertExpectationsInOrder_MissingFirstCall(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_MissingFirstCall", 1, 2, 3).Return() + mockedService.On("Test_Mock_AssertExpectationsInOrder_MissingFirstCall", 2, 3, 4).Return() + mockedService.On("Test_Mock_AssertExpectationsInOrder_MissingFirstCall", 3, 4, 5).Return() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(2, 3, 4) + mockedService.Called(3, 4, 5) + + // now assert expectations + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +func Test_Mock_AssertExpectationsInOrder_MissingMidCall(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_MissingMidCall", 1, 2, 3).Return() + mockedService.On("Test_Mock_AssertExpectationsInOrder_MissingMidCall", 2, 3, 4).Return() + mockedService.On("Test_Mock_AssertExpectationsInOrder_MissingMidCall", 3, 4, 5).Return() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(1, 2, 3) + mockedService.Called(3, 4, 5) + + // now assert expectations + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +func Test_Mock_AssertExpectationsInOrder_MissingLastCall(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_MissingLastCall", 1, 2, 3).Return() + mockedService.On("Test_Mock_AssertExpectationsInOrder_MissingLastCall", 2, 3, 4).Return() + mockedService.On("Test_Mock_AssertExpectationsInOrder_MissingLastCall", 3, 4, 5).Return() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(1, 2, 3) + mockedService.Called(2, 3, 4) + + // now assert expectations + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +func Test_Mock_AssertExpectationsInOrder_OutOfOrder(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_OutOfOrder", 1, 2, 3).Return() + mockedService.On("Test_Mock_AssertExpectationsInOrder_OutOfOrder", 2, 3, 4).Return() + mockedService.On("Test_Mock_AssertExpectationsInOrder_OutOfOrder", 3, 4, 5).Return() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(1, 2, 3) + mockedService.Called(3, 4, 5) + mockedService.Called(2, 3, 4) + + // now assert expectations + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +func Test_Mock_AssertExpectationsInOrder_OptionalLast(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_OptionalLast", 1, 2, 3).Return() + mockedService.On("Test_Mock_AssertExpectationsInOrder_OptionalLast", 2, 3, 4).Return().Maybe() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(1, 2, 3) + mockedService.Called(1, 2, 3) + + // now assert expectations + assert.True(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +func Test_Mock_AssertExpectationsInOrder_OptionalFirst(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_OptionalFirst", 1, 2, 3).Return().Maybe() + mockedService.On("Test_Mock_AssertExpectationsInOrder_OptionalFirst", 2, 3, 4).Return() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(2, 3, 4) + mockedService.Called(2, 3, 4) + + // now assert expectations + assert.True(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +func Test_Mock_AssertExpectationsInOrder_Repeatability(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability", 1, 2, 3).Return().Once() + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability", 2, 3, 4).Return().Twice() + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability", 3, 4, 5).Return().Once() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(1, 2, 3) + mockedService.Called(2, 3, 4) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + mockedService.Called(2, 3, 4) + mockedService.Called(3, 4, 5) + + // now assert expectations + assert.True(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +func Test_Mock_AssertExpectationsInOrder_Repeatability_TooFewCalls(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_TooFewCalls", 1, 2, 3).Return().Once() + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_TooFewCalls", 2, 3, 4).Return().Twice() + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_TooFewCalls", 3, 4, 5).Return().Once() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(1, 2, 3) + mockedService.Called(2, 3, 4) + mockedService.Called(3, 4, 5) + + // now assert expectations + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +func Test_Mock_AssertExpectationsInOrder_Repeatability_NotCalled(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_NotCalled", 1, 2, 3).Return().Once() + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_NotCalled", 2, 3, 4).Return().Twice() + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_NotCalled", 3, 4, 5).Return().Once() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(1, 2, 3) + mockedService.Called(3, 4, 5) + + // now assert expectations + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +func Test_Mock_AssertExpectationsInOrder_Repeatability_LastNotCalled(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_LastNotCalled", 1, 2, 3).Return().Once() + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_LastNotCalled", 2, 3, 4).Return().Twice() + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_LastNotCalled", 3, 4, 5).Return().Once() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(1, 2, 3) + mockedService.Called(2, 3, 4) + mockedService.Called(2, 3, 4) + + // now assert expectations + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +func Test_Mock_AssertExpectationsInOrder_Repeatability_CalledOutOfOrder(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_CalledOutOfOrder", 1, 2, 3).Return().Twice() + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_CalledOutOfOrder", 2, 3, 4).Return().Twice() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(1, 2, 3) + mockedService.Called(2, 3, 4) + mockedService.Called(1, 2, 3) + mockedService.Called(2, 3, 4) + + // now assert expectations + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +func Test_Mock_AssertExpectationsInOrder_Repeatability_Optional(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_Optional", 1, 2, 3).Return().Twice() + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_Optional", 2, 3, 4).Return().Twice().Maybe() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(1, 2, 3) + mockedService.Called(1, 2, 3) + + // now assert expectations + assert.True(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +func Test_Mock_AssertExpectationsInOrder_Repeatability_PartialCalledOptional(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_PartialCalledOptional", 1, 2, 3).Return().Twice() + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_PartialCalledOptional", 2, 3, 4).Return().Twice().Maybe() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(1, 2, 3) + mockedService.Called(1, 2, 3) + mockedService.Called(2, 3, 4) + + // now assert expectations + assert.True(t, mockedService.AssertExpectationsInOrder(tt)) + +} + +// this test provides code coverage of an edge case that shouldn't be hit +// in real-world-usage +func Test_Mock_AssertExpectationsInOrder_Repeatability_HackAdditionalExpectedCalls(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectationsInOrder_Repeatability_HackAdditionalExpectedCalls", 2, 3, 4).Return().Once() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + + // make the calls + mockedService.Called(2, 3, 4) + + // HACK + mockedService.expectedCalls()[0].totalCalls++ + + // now assert expectations + assert.False(t, mockedService.AssertExpectationsInOrder(tt)) + +} + func Test_Mock_TwoCallsWithDifferentArguments(t *testing.T) { var mockedService = new(TestExampleImplementation) @@ -1209,14 +1520,14 @@ func Test_Mock_AssertOptional(t *testing.T) { ms1.TheExampleMethod(1, 2, 3) tt1 := new(testing.T) - assert.Equal(t, true, ms1.AssertExpectations(tt1)) + assert.Equal(t, true, ms1.AssertExpectationsInOrder(tt1)) // Optional not called var ms2 = new(TestExampleImplementation) ms2.On("TheExampleMethod", 1, 2, 3).Maybe().Return(4, nil) tt2 := new(testing.T) - assert.Equal(t, true, ms2.AssertExpectations(tt2)) + assert.Equal(t, true, ms2.AssertExpectationsInOrder(tt2)) // Non-optional called var ms3 = new(TestExampleImplementation) @@ -1224,7 +1535,7 @@ func Test_Mock_AssertOptional(t *testing.T) { ms3.TheExampleMethod(1, 2, 3) tt3 := new(testing.T) - assert.Equal(t, true, ms3.AssertExpectations(tt3)) + assert.Equal(t, true, ms3.AssertExpectationsInOrder(tt3)) } /* @@ -1441,7 +1752,7 @@ func Test_MockMethodCalled(t *testing.T) { retArgs := m.MethodCalled("foo", "hello") require.True(t, len(retArgs) == 1) require.Equal(t, "world", retArgs[0]) - m.AssertExpectations(t) + m.AssertExpectationsInOrder(t) } func Test_MockMethodCalled_Panic(t *testing.T) { @@ -1449,7 +1760,7 @@ func Test_MockMethodCalled_Panic(t *testing.T) { m.On("foo", "hello").Panic("world panics") require.PanicsWithValue(t, "world panics", func() { m.MethodCalled("foo", "hello") }) - m.AssertExpectations(t) + m.AssertExpectationsInOrder(t) } // Test to validate fix for racy concurrent call access in MethodCalled() @@ -1551,7 +1862,7 @@ func TestArgumentMatcherToPrintMismatch(t *testing.T) { res := m.GetTime(1) require.Equal(t, "SomeTime", res) - m.AssertExpectations(t) + m.AssertExpectationsInOrder(t) } func TestClosestCallMismatchedArgumentInformationShowsTheClosest(t *testing.T) {