Skip to content
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

mock: allow testing for functional options #1023

Merged
merged 3 commits into from May 9, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
109 changes: 109 additions & 0 deletions mock/mock.go
Expand Up @@ -3,6 +3,7 @@ package mock
import (
"errors"
"fmt"
"path"
"reflect"
"regexp"
"runtime"
Expand All @@ -13,6 +14,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/pmezard/go-difflib/difflib"
"github.com/stretchr/objx"

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

Expand Down Expand Up @@ -324,6 +326,10 @@ func callString(method string, arguments Arguments, includeArgumentValues bool)
if includeArgumentValues {
var argVals []string
for argIndex, arg := range arguments {
if _, ok := arg.(*FunctionalOptionsArgument); ok {
argVals = append(argVals, fmt.Sprintf("%d: %s", argIndex, arg))
continue
}
argVals = append(argVals, fmt.Sprintf("%d: %#v", argIndex, arg))
}
argValsString = fmt.Sprintf("\n\t\t%s", strings.Join(argVals, "\n\t\t"))
Expand Down Expand Up @@ -659,6 +665,30 @@ func IsType(t interface{}) *IsTypeArgument {
return &IsTypeArgument{t: t}
}

// FunctionalOptionsArgument is a struct that contains the type and value of an functional option argument
// for use when type checking.
type FunctionalOptionsArgument struct {
name string
nbaztec marked this conversation as resolved.
Show resolved Hide resolved
value interface{}
}

// String returns the string representation of FunctionalOptionsArgument
func (f *FunctionalOptionsArgument) String() string {
return strings.Replace(fmt.Sprintf("%#v", f.value), "[]interface {}", f.name, 1)
nbaztec marked this conversation as resolved.
Show resolved Hide resolved
}

// FunctionalOptions returns an FunctionalOptionsArgument object containing the functional option type
// and the values to check of
//
// For example:
// Assert(t, FunctionalOptions("[]foo.FunctionalOption", foo.Opt1(), foo.Opt2()))
func FunctionalOptions(name string, value ...interface{}) *FunctionalOptionsArgument {
return &FunctionalOptionsArgument{
name: name,
value: value,
}
nbaztec marked this conversation as resolved.
Show resolved Hide resolved
}

// argumentMatcher performs custom argument matching, returning whether or
// not the argument is matched by the expectation fixture function.
type argumentMatcher struct {
Expand Down Expand Up @@ -798,6 +828,23 @@ func (args Arguments) Diff(objects []interface{}) (string, int) {
differences++
output = fmt.Sprintf("%s\t%d: FAIL: type %s != type %s - %s\n", output, i, reflect.TypeOf(t).Name(), reflect.TypeOf(actual).Name(), actualFmt)
}
} else if reflect.TypeOf(expected) == reflect.TypeOf((*FunctionalOptionsArgument)(nil)) {
name := expected.(*FunctionalOptionsArgument).name
t := expected.(*FunctionalOptionsArgument).value
tName := reflect.TypeOf(t).Name()
if name != reflect.TypeOf(actual).String() {
nbaztec marked this conversation as resolved.
Show resolved Hide resolved
differences++
output = fmt.Sprintf("%s\t%d: FAIL: type %s != type %s - %s\n", output, i, tName, reflect.TypeOf(actual).Name(), actualFmt)
} else {
if ef, af := assertOpts(t, actual); ef == "" && af == "" {
// match
output = fmt.Sprintf("%s\t%d: PASS: %s == %s\n", output, i, tName, tName)
} else {
// not match
differences++
output = fmt.Sprintf("%s\t%d: FAIL: %s != %s\n", output, i, af, ef)
}
}
} else {

// normal checking
Expand Down Expand Up @@ -979,3 +1026,65 @@ var spewConfig = spew.ConfigState{
type tHelper interface {
Helper()
}

func assertOpts(expected, actual interface{}) (expectedFmt, actualFmt string) {
expectedOpts := reflect.ValueOf(expected)
actualOpts := reflect.ValueOf(actual)
var expectedNames []string
for i := 0; i < expectedOpts.Len(); i++ {
expectedNames = append(expectedNames, funcName(expectedOpts.Index(i).Interface()))
}
var actualNames []string
for i := 0; i < actualOpts.Len(); i++ {
actualNames = append(actualNames, funcName(actualOpts.Index(i).Interface()))
}
if !assert.ObjectsAreEqual(expectedNames, actualNames) {
expectedFmt = fmt.Sprintf("%v", expectedNames)
actualFmt = fmt.Sprintf("%v", actualNames)
return
}

for i := 0; i < expectedOpts.Len(); i++ {
expectedOpt := expectedOpts.Index(i).Interface()
actualOpt := actualOpts.Index(i).Interface()

expectedFunc := expectedNames[i]
actualFunc := actualNames[i]
if expectedFunc != actualFunc {
expectedFmt = expectedFunc
actualFmt = actualFunc
return
}

ot := reflect.TypeOf(expectedOpt)
var expectedValues []reflect.Value
var actualValues []reflect.Value
if ot.NumIn() == 0 {
return
}

for i := 0; i < ot.NumIn(); i++ {
vt := ot.In(i).Elem()
expectedValues = append(expectedValues, reflect.New(vt))
actualValues = append(actualValues, reflect.New(vt))
}

reflect.ValueOf(expectedOpt).Call(expectedValues)
reflect.ValueOf(actualOpt).Call(actualValues)

for i := 0; i < ot.NumIn(); i++ {
if !assert.ObjectsAreEqual(expectedValues[i].Interface(), actualValues[i].Interface()) {
expectedFmt = fmt.Sprintf("%s %+v", expectedNames[i], expectedValues[i].Interface())
actualFmt = fmt.Sprintf("%s %+v", expectedNames[i], actualValues[i].Interface())
return
}
}
}

return "", ""
}

func funcName(opt interface{}) string {
n := runtime.FuncForPC(reflect.ValueOf(opt).Pointer()).Name()
return strings.TrimSuffix(path.Base(n), path.Ext(n))
}
40 changes: 40 additions & 0 deletions mock/mock_test.go
Expand Up @@ -32,6 +32,29 @@ func (i *TestExampleImplementation) TheExampleMethod(a, b, c int) (int, error) {
return args.Int(0), errors.New("Whoops")
}

type options struct {
num int
str string
}

type OptionFn func(*options)

func OpNum(n int) OptionFn {
return func(o *options) {
o.num = n
}
}

func OpStr(s string) OptionFn {
return func(o *options) {
o.str = s
}
}
func (i *TestExampleImplementation) TheExampleMethodFunctionalOptions(x string, opts ...OptionFn) error {
args := i.Called(x, opts)
return args.Error(0)
}

//go:noinline
func (i *TestExampleImplementation) TheExampleMethod2(yesorno bool) {
i.Called(yesorno)
Expand Down Expand Up @@ -1044,6 +1067,23 @@ func Test_Mock_AssertExpectationsCustomType(t *testing.T) {

}

func Test_Mock_AssertExpectationsFunctionalOptionsType(t *testing.T) {

var mockedService = new(TestExampleImplementation)

mockedService.On("TheExampleMethodFunctionalOptions", "test", FunctionalOptions("[]mock.OptionFn", OpNum(1), OpStr("foo"))).Return(nil).Once()
nbaztec marked this conversation as resolved.
Show resolved Hide resolved

tt := new(testing.T)
assert.False(t, mockedService.AssertExpectations(tt))

// make the call now
mockedService.TheExampleMethodFunctionalOptions("test", OpNum(1), OpStr("foo"))

// now assert expectations
assert.True(t, mockedService.AssertExpectations(tt))

}

nbaztec marked this conversation as resolved.
Show resolved Hide resolved
func Test_Mock_AssertExpectations_With_Repeatability(t *testing.T) {

var mockedService = new(TestExampleImplementation)
Expand Down