Skip to content

Commit

Permalink
feat: allow matching on any number of variadic args
Browse files Browse the repository at this point in the history
I've added a new `kelpie.AnyArgs[T]()` for working with variadic methods. It can be used where we really don't care if any args are passed to the method.
  • Loading branch information
adamconnelly committed Jun 6, 2024
1 parent f28119d commit 877904a
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 15 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,14 @@ secretsManagerMock.Called(
kelpie.Any[context.Context](), kelpie.Any[*secretsmanager.PutSecretValueInput]()))
```

#### Matching any arguments

Similar to the way that you can match against no parameters with `kelpie.None[T]()`, you can match that any amount of parameters are passed to a variadic function using `kelpie.AnyArgs[T]()`:

```go
mock.Setup(printer.Printf("Don't panic!", kelpie.AnyArgs[any]()).Panic("Ok!"))
```

### Interface parameters

Under the hood, Kelpie uses Go generics to allow either the actual parameter type or a Kelpie matcher to be passed in when setting up mocks or verifying expectations. For example, say we have the following method:
Expand Down
19 changes: 19 additions & 0 deletions examples/variadic_functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ func (t *VariadicFunctionsTests) Test_Parameters_NoneProvided() {
t.False(mock.Called(printer.Printf("Testing %d %d %d", 1, 2, 3)))
}

func (t *VariadicFunctionsTests) Test_Parameters_AnyMatchingWithArgumentList() {
// Arrange
mock := printer.NewMock()

mock.Setup(printer.Printf("Nothing to say", kelpie.AnyArgs[any]()).
Return("Nothing to say"))

// Act
result1 := mock.Instance().Printf("Nothing to say")
result2 := mock.Instance().Printf("Nothing to say", 1)
result3 := mock.Instance().Printf("Nothing to say", 1, "two", 3)

// Assert
t.Equal("Nothing to say", result1)
t.Equal("Nothing to say", result2)
t.Equal("Nothing to say", result3)
t.True(mock.Called(printer.Printf("Nothing to say", kelpie.AnyArgs[any]())))
}

func (t *VariadicFunctionsTests) Test_Parameters_When() {
// Arrange
mock := printer.NewMock()
Expand Down
12 changes: 11 additions & 1 deletion kelpie.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@ func Match[T any](isMatch func(arg T) bool) mocking.Matcher[T] {
}

// None is used when mocking methods that contain a variable parameter list to indicate that
// no parameters should be provided, for example: printMock.Setup(print.Printf("Testing 123", kelpie.None[any]())).
// no parameters should be provided.
//
// For example: printMock.Setup(print.Printf("Testing 123", kelpie.None[any]())).
func None[T any]() mocking.Matcher[T] {
return mocking.None[T]()
}

// AnyArgs is used when mocking methods that contain a variable parameter list when you don't
// care what arguments are passed as the variable parameter. For example:
//
// printMock.Setup(print.Printf("Testing 123", kelpie.AnyArgs[any]()).Panic("Oh no!"))
func AnyArgs[T any]() mocking.Matcher[T] {
return mocking.AnyArgs[T]()
}
57 changes: 43 additions & 14 deletions mocking/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,41 @@ type ArgumentMatcher interface {
// IsMatch returns true when the value of the argument matches the expectation.
IsMatch(other any) bool

// IsNoneMatcher is used to check whether the matcher is being used to match against an
// empty argument list for a variadic function.
IsNoneMatcher() bool
// MatcherType returns the type of the matcher.
MatcherType() MatcherType
}

// MatcherType defines the type of the matcher.
type MatcherType uint

const (
// MatcherTypeFn is a matcher that uses a custom match function.
MatcherTypeFn MatcherType = iota

// MatcherTypeNone is used to specify that no arguments are passed to a variadic function.
MatcherTypeNone

// MatcherTypeAnyArgs is used to specify that any number of arguments can be passed to a variadic function.
MatcherTypeAnyArgs

// MatcherTypeVariadic is a matcher that matches the variable argument list passed to a variadic function.
MatcherTypeVariadic
)

// Matcher is used to match an argument in a method invocation.
type Matcher[T any] struct {
MatchFn func(input T) bool
isNoneMatcher bool
MatchFn func(input T) bool
matcherType MatcherType
}

// IsMatch returns true if other is a match to the expectation.
func (i Matcher[T]) IsMatch(other any) bool {
return i.MatchFn(other.(T))
}

// IsNoneMatcher returns true if this matcher matches against variadic function empty argument lists.
func (i Matcher[T]) IsNoneMatcher() bool {
return i.isNoneMatcher
// MatcherType returns the matcher's type.
func (i Matcher[T]) MatcherType() MatcherType {
return i.matcherType
}

type variadicMatcher struct {
Expand All @@ -38,9 +54,9 @@ func Variadic(matchers []ArgumentMatcher) ArgumentMatcher {
return &variadicMatcher{matchers: matchers}
}

// IsNoneMatcher always returns false for variadicMatcher.
func (v *variadicMatcher) IsNoneMatcher() bool {
return false
// MatcherType returns the type of the matcher.
func (v *variadicMatcher) MatcherType() MatcherType {
return MatcherTypeVariadic
}

// IsMatch returns true if other is a match to the expectation.
Expand All @@ -63,8 +79,13 @@ func (v *variadicMatcher) IsMatch(other any) bool {
}
}

if len(v.matchers) == 1 && v.matchers[0].IsNoneMatcher() {
return len(args) == 0
if len(v.matchers) == 1 {
switch v.matchers[0].MatcherType() {
case MatcherTypeNone:
return len(args) == 0
case MatcherTypeAnyArgs:
return true
}
}

if len(args) != len(v.matchers) {
Expand All @@ -83,7 +104,15 @@ func (v *variadicMatcher) IsMatch(other any) bool {
// None is used to indicate that no arguments should be passed to a variadic function.
func None[T any]() Matcher[T] {
return Matcher[T]{
isNoneMatcher: true,
matcherType: MatcherTypeNone,
}
}

// AnyArgs is used to indicate that any amount of arguments (including no arguments) should
// be passed to a variadic function.
func AnyArgs[T any]() Matcher[T] {
return Matcher[T]{
matcherType: MatcherTypeAnyArgs,
}
}

Expand Down
10 changes: 10 additions & 0 deletions mocking/matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ func (t *MatcherTests) Test_VariadicMatcher_MatchesWhenParametersEmpty() {
inputs: []any{"testing"},
isMatch: false,
},
"Any args matcher matches empty input": {
matchers: []mocking.ArgumentMatcher{mocking.AnyArgs[any]()},
inputs: []any{},
isMatch: true,
},
"Any args matcher matches non-empty input": {
matchers: []mocking.ArgumentMatcher{mocking.AnyArgs[any]()},
inputs: []any{"testing"},
isMatch: true,
},
"Matchers empty but arguments provided": {
matchers: []mocking.ArgumentMatcher{},
inputs: []any{"testing", 1, 2, 3},
Expand Down

0 comments on commit 877904a

Please sign in to comment.