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 all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions mock/mock.go
Original file line number Diff line number Diff line change
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,34 @@ 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 {
value interface{}
}

// String returns the string representation of FunctionalOptionsArgument
func (f *FunctionalOptionsArgument) String() string {
var name string
tValue := reflect.ValueOf(f.value)
if tValue.Len() > 0 {
name = "[]" + reflect.TypeOf(tValue.Index(0).Interface()).String()
}

return strings.Replace(fmt.Sprintf("%#v", f.value), "[]interface {}", name, 1)
}

// 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(value ...interface{}) *FunctionalOptionsArgument {
return &FunctionalOptionsArgument{
value: value,
}
}

// 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 +832,29 @@ 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)) {
t := expected.(*FunctionalOptionsArgument).value

var name string
tValue := reflect.ValueOf(t)
if tValue.Len() > 0 {
name = "[]" + reflect.TypeOf(tValue.Index(0).Interface()).String()
}

tName := reflect.TypeOf(t).Name()
if name != reflect.TypeOf(actual).String() && tValue.Len() != 0 {
differences++
output = fmt.Sprintf("%s\t%d: FAIL: type %s != type %s - %s\n", output, i, tName, reflect.TypeOf(actual).Name(), actualFmt)
Comment on lines +845 to +847

Choose a reason for hiding this comment

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

I believe I'm following the test setup nearly the same (options are from another package) and I noticed 2 things.

  1. tName is empty for me but reflect.TypeOf(tValue.Index(0).Interface()).String() has the same type as reflect.TypeOf(actual).String()
  2. name has the prefix [] so I'm hitting this branch

Curious the choice for tName and reflect.TypeOf(actual).Name() in the log here instead of name and reflect.TypeOf(actual).String() that is used in the conditional.

It was hard for me to debug as the logs showed empty string is not the same type as my option type. When debugging I found that name was really close except for the prefixed []

I might follow up with a PR to fix the logged out line here if it was just an accident.

} 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 +1036,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))
}
57 changes: 57 additions & 0 deletions mock/mock_test.go
Original file line number Diff line number Diff line change
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,40 @@ func Test_Mock_AssertExpectationsCustomType(t *testing.T) {

}

func Test_Mock_AssertExpectationsFunctionalOptionsType(t *testing.T) {

var mockedService = new(TestExampleImplementation)

mockedService.On("TheExampleMethodFunctionalOptions", "test", FunctionalOptions(OpNum(1), OpStr("foo"))).Return(nil).Once()

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_AssertExpectationsFunctionalOptionsType_Empty(t *testing.T) {

var mockedService = new(TestExampleImplementation)

mockedService.On("TheExampleMethodFunctionalOptions", "test", FunctionalOptions()).Return(nil).Once()

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

// make the call now
mockedService.TheExampleMethodFunctionalOptions("test")

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

}

func Test_Mock_AssertExpectations_With_Repeatability(t *testing.T) {

var mockedService = new(TestExampleImplementation)
Expand Down