Skip to content

mock: [feature] dynamic return values #1726

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 15 commits into
base: master
Choose a base branch
from

Conversation

jlou2u
Copy link

@jlou2u jlou2u commented Apr 5, 2025

Summary

Adds RunWithReturn which is essentially Run but adds Arguments as the return type and propagates them.

Used this as a starting point #742

Changes

  • Adds RunWithReturn

Motivation

Helps write simple tests that can dynamically calculate return values.

Example (from test)

counter := 0
mockedService.On("TheExampleMethod", Anything, Anything, Anything).
	RunWithReturn(func(args Arguments) Arguments {
		counter++
		a, b, c := args[0].(int), args[1].(int), args[2].(int)
		assert.IsType(t, 1, a)
		assert.IsType(t, 1, b)
		assert.IsType(t, 1, c)
		return Arguments{counter}
	}).
	Twice()

answer, _ := mockedService.TheExampleMethod(2, 4, 5)
assert.Equal(t, 1, answer)

answer, _ = mockedService.TheExampleMethod(44, 4, 5)
assert.Equal(t, 2, answer)

Related issues

#742

gburt and others added 12 commits April 5, 2025 08:29
Co-authored-by: Bracken <abdawson@gmail.com>
The comments for the require package were just copied over
from the assert package when generating the functions.
This could lead to confusion because
1. The code-examples were showing examples using the
assert package instead of the require package
2. The function-documentation was not mentioning that
the functions were calling `t.FailNow()` which is some
critical information when using this package.
Copy link
Collaborator

@brackendawson brackendawson left a comment

Choose a reason for hiding this comment

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

This should override any previous calls to Return but also be overridden by any subsequent call to Return:

package kata_test

import (
	"testing"

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

type myMock struct {
	mock.Mock
}

func (m *myMock) Do() string {
	return m.Called().String(0)
}

func TestIfy(t *testing.T) {
	m := &myMock{}
	m.On("Do").Return("one").Return("two")
	assert.Equal(t, "two", m.Do())

	m = &myMock{}
	m.On("Do").Return("one").RunWithReturn(func(args mock.Arguments) mock.Arguments {
		return mock.Arguments{"two"}
	})
	assert.Equal(t, "two", m.Do())

	m = &myMock{}
	m.On("Do").RunWithReturn(func(args mock.Arguments) mock.Arguments {
		return mock.Arguments{"one"}
	}).Return("two")
	assert.Equal(t, "two", m.Do())
}

This is why I suggested calling it ReturnFn, so that folks understand its purpose to to set return values. The purpose of Run is to implement side effects.

@@ -29,7 +29,7 @@ type TestExampleImplementation struct {

func (i *TestExampleImplementation) TheExampleMethod(a, b, c int) (int, error) {
args := i.Called(a, b, c)
return args.Int(0), errors.New("Whoops")
Copy link
Author

Choose a reason for hiding this comment

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

this looked wrong -- not sure why this would always return an error?

@jlou2u jlou2u requested a review from brackendawson May 16, 2025 15:06
Copy link
Collaborator

@brackendawson brackendawson left a comment

Choose a reason for hiding this comment

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

Good work so far. 👍

This also causes Mock.Unset to misbehave:

	m = &myMock{}
	m.On("Do").ReturnFn(func(args mock.Arguments) mock.Arguments { return mock.Arguments{"one"} })
	m.On("Do").Return("two")
	m.On("Do").Return("two").Unset() // Unset is an awful Method
	assert.Equal(t, "one", m.Do())

Mock.Unset is using Arguments.Diff to find matching calls, but because Arguments is empty it implicitly matches. Unset should probably never match any Call when Call.ReturnArguments is empty and Call.returnFn is non-nil. Unset should probably panic when called on a Call when Call.ReturnArguments is empty and Call.returnFn is non-nil, this is because functions are not comparable.

// ReturnFn sets a handler to be called before returning.
//
// Mock.On("MyMethod", arg1, arg2).ReturnFn(func(args Arguments) Arguments {
// return Arguments{args.Get(0) + args.Get(1)}
Copy link
Collaborator

Choose a reason for hiding this comment

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

The example should be valid Go, did you mean Int rather than Get?

Suggested change
// return Arguments{args.Get(0) + args.Get(1)}
// return Arguments{args.Int(0) + args.Int(1)}

returnFn := call.returnFn
m.mutex.Unlock()

if returnFn != nil {
Copy link
Collaborator

@brackendawson brackendawson May 23, 2025

Choose a reason for hiding this comment

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

Because call.ReturnArguments is exported it should probably be checked by this condition rather than returnFn, eg:

	m = &myMock{}
	m.On("Do").ReturnFn(func(args mock.Arguments) mock.Arguments { return mock.Arguments{"two"} })
	m.ExpectedCalls[0].ReturnArguments = mock.Arguments{"one"}
	assert.Equal(t, "one", m.Do())

ie. Call.ReturnArguments should always override Call.returnFn

@dolmen dolmen added enhancement pkg-mock Any issues related to Mock labels May 23, 2025
@dolmen dolmen changed the title Feature/dynamic return values mock: [feature] dynamic return values May 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement pkg-mock Any issues related to Mock
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants