镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

mock: [feature] dynamic return values - #1726

Open
jlou2u wants to merge 18 commits into
stretchr:masterfrom
jlou2u:feature/dynamic-return-values
Open

jlou2u wants to merge 18 commits into
stretchr:masterfrom
jlou2u:feature/dynamic-return-values

Conversation

@jlou2u

@jlou2u jlou2u commented Apr 5, 2025

Copy link
Copy Markdown

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.

@brackendawson brackendawson left a comment

Copy link
Copy Markdown
Collaborator

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.

Comment thread mock/mock.go Resolved
Comment thread mock/mock_test.go Resolved
@jlou2u
jlou2u requested a review from brackendawson May 16, 2025 15:06

@brackendawson brackendawson left a comment

Copy link
Copy Markdown
Collaborator

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.

Comment thread mock/mock.go
// 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
Copy Markdown
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)}

Comment thread mock/mock.go
returnFn := call.returnFn
m.mutex.Unlock()

if returnFn != nil {

@brackendawson brackendawson May 23, 2025 •

Copy link
Copy Markdown
Collaborator

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 help wanted pkg-mock Any issues related to Mock

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants