Skip to content

fix: fix IsMethodCallable #1718

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

Conversation

gopherjit
Copy link

@gopherjit gopherjit commented Mar 25, 2025

fixes method IsMethodCallable and respective unit test

Summary

fixes the method IsMethodCallable and respective unit test

Changes

calling findExpectedCall to check if the call is expected and repeatability is not equal to -1, if Yes return true, otherwise return true and false for the cases if the provided method was called and not called respectively.
fix respective unit test

Motivation

implementation of IsMethodCallable is not correct also the unit test is not written correctly

Related issues

Closes #1712

fixed method and unit test
@gopherjit
Copy link
Author

@brackendawson please review

mock/mock.go Outdated
func (m *Mock) IsMethodCallable(t TestingT, methodName string, arguments ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
func (m *Mock) IsMethodCallable(methodName string, arguments ...interface{}) bool {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know mocks package very well, but I'm surprised to see a signature change on a public API.

I mean t TestingT was the first argument was removed 🤔

Based on old code, I would say it was not used except for marking a method with t.Helper but it's not a reason to remove it I think

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I mentioned in the issue that the TestingT is pointless here, but we cannot remove it because that is a breaking change.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ccoVeille for looking at the code, I understand this is a breaking change for the API, but t testing.T was redundant for this function, we can discuss if we can accommodate a breaking change in the API with the benefit of little cleaner code, it is always a tradeoff.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here is what I would have expected

Suggested change
func (m *Mock) IsMethodCallable(t TestingT, methodName string, arguments ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
func (m *Mock) IsMethodCallable(methodName string, arguments ...interface{}) bool {
func (m *Mock) IsMethodCallable(_ TestingT, methodName string, arguments ...interface{}) bool {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brackendawson @ccoVeille I have reintroduced the testing.T type argument.

introduce blank identifier instead of t
Comment on lines 1717 to 1724
// Case 1: Method is expected and has remaining repeatability
mock.Mock.ExpectedCalls = append(mock.Mock.ExpectedCalls,
&Call{Method: "TestMethod", Repeatability: 1, Arguments: Arguments{"TestArg"}})
assert.True(t, mock.IsMethodCallable(t, "TestMethod", "TestArg"))

assert.False(t, mockedService.IsMethodCallable(t, "Test_Mock_IsMethodCallable", arg2))
// Case 2: Method is expected but has no repeatability left
mock.Mock.ExpectedCalls[0].Repeatability = -1
assert.False(t, mock.IsMethodCallable(t, "TestMethod", "TestArg"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, I'm not able to tell if the new tests are OK.

I mean there were a lot of test that were removed, and I'm unsure why.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 1722 to 1724
// Case 2: Method is expected but has no repeatability left
mock.Mock.ExpectedCalls[0].Repeatability = -1
assert.False(t, mock.IsMethodCallable(t, "TestMethod", "TestArg"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I feel like these two cases should be in separated t.Run

Here I feel like the code of the test is strange because it reuses result from previous one.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the tests are now in separate test runners

ccoVeille
ccoVeille previously approved these changes Mar 26, 2025
Copy link
Collaborator

@ccoVeille ccoVeille left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍 in terms of code.

Let's wait for maintainers' feedbacks now

mock/mock.go Outdated
}
return false
// return false if call was not expected
return m.methodWasCalled(methodName, arguments)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to call methodWasCalled because findExpectedCall will not return an expected call which has been fully satisfied.

I found this regression by writing the following testcase. I'd like to see something similar added in this PR.

package kata_test

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

type MyMock struct {
	mock.Mock
}

func (m *MyMock) Do(i int) {
	m.Called(i)
}

func TestIfy(t *testing.T) {
	for name, expectedArg := range map[string]any{
		"1":              1,
		"Anything":       mock.Anything,
		"MatchedBy":      mock.MatchedBy(func(i int) bool { return i == 1 }),
		"AnythingOfType": mock.AnythingOfType("int"),
		"IsType":         mock.IsType(0),
	} {
		t.Run(name, func(t *testing.T) {
			expectedArg := expectedArg
			t.Parallel()

			m := &MyMock{}

			m.On("Do", expectedArg).Return().Times(2)
			assert.True(t, m.IsMethodCallable(t, "Do", 1))
			m.Do(1)
			assert.True(t, m.IsMethodCallable(t, "Do", 1))
			m.Do(1)
			assert.False(t, m.IsMethodCallable(t, "Do", 1))
		})
	}
}

After fixing this I think isArgsEqual() and methodWasCalled() are unused and should be removed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed usage of methodWasCalled in "IsMethodCallable"
I have removed isArgsEqual, didn't remove methodWasCalled as it is used at some other places.
The expected changes in unit tests are not clear to me.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me annotate the test with some comments, hopefully it becomes clear:

func TestIfy(t *testing.T) {
	// Test this with each kind of expected argumnet. It works with `1` (raw value)
	// in tstify v1.10.0 but not with any of the other kinds.
	for name, expectedArg := range map[string]any{
		"1":              1,
		"Anything":       mock.Anything,
		"MatchedBy":      mock.MatchedBy(func(i int) bool { return i == 1 }),
		"AnythingOfType": mock.AnythingOfType("int"),
		"IsType":         mock.IsType(0),
	} {
		t.Run(name, func(t *testing.T) {
			// Copy expectedArg to local scope before calling Parallel, solves the
			// capturing loop variable issue for go <v1.22.
			expectedArg := expectedArg
			t.Parallel()

			// Make a mock as usual
			m := &MyMock{}

			// Prime the mock as usual
			m.On("Do", expectedArg).Return().Times(2)
			
			// The "Do" call is now expected, it is expected twice.
			assert.True(t, m.IsMethodCallable(t, "Do", 1))
			
			// Call Do once
			m.Do(1)
			
			// The "Do" call is now expected once
			assert.True(t, m.IsMethodCallable(t, "Do", 1))
			
			// Call it again
			m.Do(1)
			
			// The "Do" call is now not expected, so it is not callable.
			assert.False(t, m.IsMethodCallable(t, "Do", 1))
		})
	}
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @brackendawson, I have added the tests in similar fashion as suggested, please let me know if any other improvements can be made.

removed isArgsEqual as well
convert unit tests of IsMethodCallable to table tests format
@gopherjit
Copy link
Author

@brackendawson please review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Mock.IsMethodCallable doesn't work with dynamic expected arguments
3 participants