testify icon indicating copy to clipboard operation
testify copied to clipboard

Support for Variadic Methods in Mocks without modifying mock structure

Open UnaffiliatedCode opened this issue 3 years ago • 1 comments

Story: As a mock implementer, I have several methods with variadic arg signature. Currently we can get around this problem by removing the v... from the underlying mock item (the result of the unroll variadic flag with mockery).

I have read the documentation found:

  • https://github.com/stretchr/testify/issues/101
  • https://github.com/stretchr/testify/issues/897
  • https://github.com/stretchr/testify/issues/1148

Here is ALL the code to replicate on your local development machine:

Target Variadic Interface
type TestStructure interface  {
	VariadicTest(startArg string, v ...interface{}) 
}
Original Mock object from mockery
// IncorrectTestStructure is an autogenerated mock type for the IncorrectTestStructure type
type IncorrectTestStructure struct {
	mock.Mock
}

// VariadicTest provides a mock function with a variadic method signature
func (_m *IncorrectTestStructure) VariadicTest(startArg string, v ...interface{}) {
	var _ca []interface{}
	_ca = append(_ca, startArg)
	_ca = append(_ca, v...)
	_m.Called(_ca...)
}

type mockConstructorTestingTNewIncorrectTestStructure interface {
	mock.TestingT
	Cleanup(func())
}
// NewIncorrectTestStructure creates a new instance of CorrectedTestStructure. 
// It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewIncorrectTestStructure(t mockConstructorTestingTNewIncorrectTestStructure) *IncorrectTestStructure {
	mock := &IncorrectTestStructure{}
	mock.Mock.Test(t)

	t.Cleanup(func() { mock.AssertExpectations(t) })

	return mock
}
Corrected Mock object from mockery
// CorrectedTestStructure is an autogenerated mock type for the CorrectedTestStructure type
type CorrectedTestStructure struct {
	mock.Mock
}

// VariadicTest provides a mock function with a variadic method signature
func (_m *CorrectedTestStructure) VariadicTest(startArg string, v ...interface{}) {
	var _ca []interface{}
	_ca = append(_ca, startArg)
	_ca = append(_ca, v)
	_m.Called(_ca...)
}

type mockConstructorTestingTNewCorrectedTestStructure interface {
	mock.TestingT
	Cleanup(func())
}

// NewCorrectedTestStructure creates a new instance of CorrectedTestStructure. 
// It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewCorrectedTestStructure(t mockConstructorTestingTNewCorrectedTestStructure) *CorrectedTestStructure {
	mock := &CorrectedTestStructure{}
	mock.Mock.Test(t)

	t.Cleanup(func() { mock.AssertExpectations(t) })

	return mock
}
A simple parent structure
type ParentStructure struct {
	childObject TestStructure
}

func (m ParentStructure) VariadicTest(startArg string, v ...interface{}) {
	m.childObject.VariadicTest(startArg,v...)
}
Test showing Incorrect Structure from mockery failing

func TestMinimumReplication_IncorrectTestStructure_Simple_Fails(t *testing.T) {
	tests := []struct {
		name         string
		mockArguments []interface{}
		providedArguments []interface{}
	}{
		{name: "1 +1 Variadic Provided", mockArguments: []interface{}{mock.Anything,mock.Anything},providedArguments: []interface{}{"tester"} },
		{name: "1 +2 Variadic Provided", mockArguments: []interface{}{mock.Anything,mock.Anything},providedArguments: []interface{}{"tester", "Test 2"} },
		{name: "1 +3 Variadic Provided", mockArguments: []interface{}{mock.Anything,mock.Anything},providedArguments: []interface{}{"tester", "Test 2", "Test 3"} },
	}
	for _, tt := range tests {
		mockedObject := new(IncorrectTestStructure)
		mockedObject.On("VariadicTest",tt.mockArguments...).Return()
		testTarget := ParentStructure{childObject: mockedObject}
		testTarget.VariadicTest("Test call",tt.providedArguments...)
	}
}

func TestMinimumReplication_IncorrectTestStructure_MatchedBy_Fails(t *testing.T) {
	tests := []struct {
		name         string
		providedArguments []interface{}
	}{
		{name: "1 +1 Variadic Provided", providedArguments: []interface{}{"tester"} },
		{name: "1 +2 Variadic Provided", providedArguments: []interface{}{"tester", "Test 2"} },
		{name: "1 +3 Variadic Provided", providedArguments: []interface{}{"tester", "Test 2", "Test 3"} },
	}
	for _, tt := range tests {
		mockedObject := new(IncorrectTestStructure)
		mockedObject.On("VariadicTest",mock.Anything, mock.MatchedBy(func(args []interface{}) bool {
			return len(args) >= 1
		}))
		testTarget := ParentStructure{childObject: mockedObject}
		testTarget.VariadicTest("Test call",tt.providedArguments...)
	}	
}
Test showing Corrected Structure from mockery SUCCEEDING

func TestMinimumReplication_CorrectedTestStructure_Simple_Succeeds(t *testing.T) {
	tests := []struct {
		name         string
		mockArguments []interface{}
		providedArguments []interface{}
	}{
		{name: "1 +1 Variadic Provided", mockArguments: []interface{}{mock.Anything,mock.Anything},providedArguments: []interface{}{"tester"} },
		{name: "1 +2 Variadic Provided", mockArguments: []interface{}{mock.Anything,mock.Anything},providedArguments: []interface{}{"tester", "Test 2"} },
		{name: "1 +3 Variadic Provided", mockArguments: []interface{}{mock.Anything,mock.Anything},providedArguments: []interface{}{"tester", "Test 2", "Test 3"} },
	}
	for _, tt := range tests {
		mockedObject := new(CorrectedTestStructure)
		mockedObject.On("VariadicTest",tt.mockArguments...).Return()
		testTarget := ParentStructure{childObject: mockedObject}
		testTarget.VariadicTest("Test call",tt.providedArguments...)
	}
}

func TestMinimumReplication_CorrectedTestStructure_MatchedBy_Succeeds(t *testing.T) {
	tests := []struct {
		name         string
		providedArguments []interface{}
	}{
		{name: "1 +1 Variadic Provided", providedArguments: []interface{}{"tester"} },
		{name: "1 +2 Variadic Provided", providedArguments: []interface{}{"tester", "Test 2"} },
		{name: "1 +3 Variadic Provided", providedArguments: []interface{}{"tester", "Test 2", "Test 3"} },
	}
	for _, tt := range tests {
		mockedObject := new(CorrectedTestStructure)
		mockedObject.On("VariadicTest",mock.Anything, mock.MatchedBy(func(args []interface{}) bool {
			return len(args) >= 1
		}))
		testTarget := ParentStructure{childObject: mockedObject}
		testTarget.VariadicTest("Test call",tt.providedArguments...)
	}	
}

Based off this information, it appears that we are currently using a patch solution as opposed to updating the underlying method. It looks like this could be resolved as part of here: https://github.com/stretchr/testify/blob/181cea6eab8b2de7071383eca4be32a424db38dd/mock/mock.go#L332

or here:

func (m *Mock) findExpectedCall(method string, arguments ...interface{}) (int, *Call) {
	var expectedCall *Call

	for i, call := range m.ExpectedCalls {
		if call.Method == method {
			_, diffCount := call.Arguments.Diff(arguments)
			if diffCount == 0 {
				expectedCall = call
				if call.Repeatability > -1 {
					return i, call
				}
			}
		}
	}

	return -1, expectedCall
}

In the case of a variadic signature, we can compare the final argument definition to determine if the method is variadic. In the case of methods, we can directly check via reflecting into the targetMethod (of type reflect.Method) and looking here: targetMethod.Func.Type().IsVariadic() If we don't have access to that piece of information, we can check the Name + Kind(). This will come out as nil name, and of slice type.

Using the previous example, we can verify the accessibility via the following test (using the previously defined test structure)

Test Method for identifying variadic position
func Test_MethodRead(t *testing.T) {
	ex := ParentStructure{}
	target := reflect.TypeOf(ex)
	t.Log("Methods")
	for i := 0; i<target.NumMethod(); i++ {
		targetMethod := target.Method(i)
		targetType := targetMethod.Func.Type()
		t.Logf("%s : Is Variadic? : %v",targetMethod.Name,targetType.IsVariadic())
		methodType := targetMethod.Type
		if (targetType.IsVariadic()) {
			parameterIndex := methodType.NumIn() -1
			targetInputArg := methodType.In(parameterIndex)
			t.Logf("%s : %s",targetMethod.Name,targetInputArg.Name())
			t.Logf("%s : %s : %v",targetMethod.Name,targetInputArg.Name(),targetInputArg.Kind()) 
			t.Logf("%s : %s : Type : %s",targetMethod.Name,targetInputArg.Name(),targetInputArg.String()) 
		}
	}
	t.FailNow() // Forces logs to display
}

Personally, I think the implementation of: mockedObject.On("VariadicTest",mock.Anything,mock.Variadic(mock.Anything)).Return() would make sense based on the current implementation. An alternative could be a new method which does (or does not) check arguments mockedObject.OnAllCalls("VariadicTest").Return()

UnaffiliatedCode avatar Sep 22 '22 18:09 UnaffiliatedCode

edit: minor update due to incorrect label on summary: "Test showing Corrected Structure from mockery SUCCEEDING"

UnaffiliatedCode avatar Sep 23 '22 14:09 UnaffiliatedCode

Any updates guys? 😢

MrNocTV avatar Jun 12 '23 03:06 MrNocTV

Are there any plans to implement this?

UnaffiliatedCode avatar Jul 26 '23 14:07 UnaffiliatedCode