mirror of https://github.com/stretchr/testify.git
Allow custom matcher functions to return error strings displayed on a failed match
parent
f35b8ab0b5
commit
b59ea01145
53
mock/mock.go
53
mock/mock.go
|
@ -556,6 +556,10 @@ const (
|
|||
Anything = "mock.Anything"
|
||||
)
|
||||
|
||||
var (
|
||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
)
|
||||
|
||||
// AnythingOfTypeArgument is a string that contains the type of an argument
|
||||
// for use when type checking. Used in Diff and Assert.
|
||||
type AnythingOfTypeArgument string
|
||||
|
@ -576,7 +580,7 @@ type argumentMatcher struct {
|
|||
fn reflect.Value
|
||||
}
|
||||
|
||||
func (f argumentMatcher) Matches(argument interface{}) bool {
|
||||
func (f argumentMatcher) Matches(argument interface{}) error {
|
||||
expectType := f.fn.Type().In(0)
|
||||
expectTypeNilSupported := false
|
||||
switch expectType.Kind() {
|
||||
|
@ -597,25 +601,51 @@ func (f argumentMatcher) Matches(argument interface{}) bool {
|
|||
}
|
||||
if argType == nil || argType.AssignableTo(expectType) {
|
||||
result := f.fn.Call([]reflect.Value{arg})
|
||||
return result[0].Bool()
|
||||
|
||||
var matchError error
|
||||
switch {
|
||||
case result[0].Type().Kind() == reflect.Bool:
|
||||
if !result[0].Bool() {
|
||||
matchError = fmt.Errorf("not matched by %s", f)
|
||||
}
|
||||
case result[0].Type().Implements(errorType):
|
||||
if !result[0].IsNil() {
|
||||
matchError = result[0].Interface().(error)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Errorf("matcher function of unknown type: %s", result[0].Type().Kind()))
|
||||
}
|
||||
|
||||
return matchError
|
||||
}
|
||||
return false
|
||||
return fmt.Errorf("unexpected type for %s", f)
|
||||
}
|
||||
|
||||
func (f argumentMatcher) String() string {
|
||||
return fmt.Sprintf("func(%s) bool", f.fn.Type().In(0).Name())
|
||||
return fmt.Sprintf("func(%s) %s", f.fn.Type().In(0).String(), f.fn.Type().Out(0).String())
|
||||
}
|
||||
|
||||
func (f argumentMatcher) GoString() string {
|
||||
return fmt.Sprintf("MatchedBy(%s)", f)
|
||||
}
|
||||
|
||||
// MatchedBy can be used to match a mock call based on only certain properties
|
||||
// from a complex struct or some calculation. It takes a function that will be
|
||||
// evaluated with the called argument and will return true when there's a match
|
||||
// and false otherwise.
|
||||
// evaluated with the called argument and will return either a boolean (true
|
||||
// when there's a match and false otherwise) or an error (nil when there's a
|
||||
// match and error holding the failure message otherwise).
|
||||
//
|
||||
// Example:
|
||||
// m.On("Do", MatchedBy(func(req *http.Request) bool { return req.Host == "example.com" }))
|
||||
// m.On("Do", MatchedBy(func(req *http.Request) (err error) {
|
||||
// if req.Host != "example.com" {
|
||||
// err = errors.New("host was not example.com")
|
||||
// }
|
||||
// return
|
||||
// })
|
||||
//
|
||||
// |fn|, must be a function accepting a single argument (of the expected type)
|
||||
// which returns a bool. If |fn| doesn't match the required signature,
|
||||
// which returns a bool or error. If |fn| doesn't match the required signature,
|
||||
// MatchedBy() panics.
|
||||
func MatchedBy(fn interface{}) argumentMatcher {
|
||||
fnType := reflect.TypeOf(fn)
|
||||
|
@ -626,8 +656,9 @@ func MatchedBy(fn interface{}) argumentMatcher {
|
|||
if fnType.NumIn() != 1 {
|
||||
panic(fmt.Sprintf("assert: arguments: %s does not take exactly one argument", fn))
|
||||
}
|
||||
if fnType.NumOut() != 1 || fnType.Out(0).Kind() != reflect.Bool {
|
||||
panic(fmt.Sprintf("assert: arguments: %s does not return a bool", fn))
|
||||
|
||||
if fnType.NumOut() != 1 || (fnType.Out(0).Kind() != reflect.Bool && !fnType.Out(0).Implements(errorType)) {
|
||||
panic(fmt.Sprintf("assert: arguments: %s does not return a bool or a error", fn))
|
||||
}
|
||||
|
||||
return argumentMatcher{fn: reflect.ValueOf(fn)}
|
||||
|
@ -687,11 +718,11 @@ func (args Arguments) Diff(objects []interface{}) (string, int) {
|
|||
}
|
||||
|
||||
if matcher, ok := expected.(argumentMatcher); ok {
|
||||
if matcher.Matches(actual) {
|
||||
if matchError := matcher.Matches(actual); matchError == nil {
|
||||
output = fmt.Sprintf("%s\t%d: PASS: %s matched by %s\n", output, i, actualFmt, matcher)
|
||||
} else {
|
||||
differences++
|
||||
output = fmt.Sprintf("%s\t%d: PASS: %s not matched by %s\n", output, i, actualFmt, matcher)
|
||||
output = fmt.Sprintf("%s\t%d: FAIL: %s %s\n", output, i, actualFmt, matchError)
|
||||
}
|
||||
} else if reflect.TypeOf(expected) == reflect.TypeOf((*AnythingOfTypeArgument)(nil)).Elem() {
|
||||
|
||||
|
|
|
@ -1258,7 +1258,7 @@ func Test_Arguments_Diff_WithArgMatcher(t *testing.T) {
|
|||
|
||||
diff, count = args.Diff([]interface{}{"string", false, true})
|
||||
assert.Equal(t, 1, count)
|
||||
assert.Contains(t, diff, `(bool=false) not matched by func(int) bool`)
|
||||
assert.Contains(t, diff, `(bool=false) unexpected type for func(int) bool`)
|
||||
|
||||
diff, count = args.Diff([]interface{}{"string", 123, false})
|
||||
assert.Contains(t, diff, `(int=123) matched by func(int) bool`)
|
||||
|
@ -1268,6 +1268,31 @@ func Test_Arguments_Diff_WithArgMatcher(t *testing.T) {
|
|||
assert.Contains(t, diff, `No differences.`)
|
||||
}
|
||||
|
||||
func Test_Arguments_Diff_WithArgMatcherReturningError(t *testing.T) {
|
||||
matchFn := func(a int) (err error) {
|
||||
if a != 123 {
|
||||
err = errors.New("did not match")
|
||||
}
|
||||
return
|
||||
}
|
||||
var args = Arguments([]interface{}{"string", MatchedBy(matchFn), true})
|
||||
|
||||
diff, count := args.Diff([]interface{}{"string", 124, true})
|
||||
assert.Equal(t, 1, count)
|
||||
assert.Contains(t, diff, `(int=124) did not match`)
|
||||
|
||||
diff, count = args.Diff([]interface{}{"string", false, true})
|
||||
assert.Equal(t, 1, count)
|
||||
assert.Contains(t, diff, `(bool=false) unexpected type for func(int) error`)
|
||||
|
||||
diff, count = args.Diff([]interface{}{"string", 123, false})
|
||||
assert.Contains(t, diff, `(int=123) matched by func(int) error`)
|
||||
|
||||
diff, count = args.Diff([]interface{}{"string", 123, true})
|
||||
assert.Equal(t, 0, count)
|
||||
assert.Contains(t, diff, `No differences.`)
|
||||
}
|
||||
|
||||
func Test_Arguments_Assert(t *testing.T) {
|
||||
|
||||
var args = Arguments([]interface{}{"string", 123, true})
|
||||
|
@ -1444,7 +1469,7 @@ func TestArgumentMatcherToPrintMismatch(t *testing.T) {
|
|||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
matchingExp := regexp.MustCompile(
|
||||
`\s+mock: Unexpected Method Call\s+-*\s+GetTime\(int\)\s+0: 1\s+The closest call I have is:\s+GetTime\(mock.argumentMatcher\)\s+0: mock.argumentMatcher\{.*?\}\s+Diff:.*\(int=1\) not matched by func\(int\) bool`)
|
||||
`\s+mock: Unexpected Method Call\s+-*\s+GetTime\(int\)\s+0: 1\s+The closest call I have is:\s+GetTime\(mock.argumentMatcher\)\s+0: MatchedBy\(func\(int\) bool\)\s+Diff:.*\(int=1\) not matched by func\(int\) bool`)
|
||||
assert.Regexp(t, matchingExp, r)
|
||||
}
|
||||
}()
|
||||
|
|
Loading…
Reference in New Issue