diff --git a/README.md b/README.md index aa52ea9..1a715aa 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,8 @@ func (t *VariadicFunctionsTests) Test_Parameters_ExactMatch() { } ``` +#### Mixing exact and custom matching + Because of the way generics work, you can't mix exact matching with custom matching. So for example the following will work: ```go @@ -242,6 +244,8 @@ mock.Setup(printer.Printf("Hello %s. This is %s, %s.", "Dolly", kelpie.Any[strin Return("Hello Dolly. This is Louis, Dolly.")) ``` +#### Mixing argument types + If your variadic parameter is `...any` or `...interface{}`, and you try to pass in multiple different types of argument, the Go compiler can't infer the types for you. Here's an example: ```go @@ -255,6 +259,30 @@ To fix this, just specify the type parameters: mock.Called(printer.Printf[string, any]("Hello world!", "One", 2, 3.0)) ``` +#### Matching no arguments + +If you want to match that a variadic function call is made with no arguments provided, you can use `kelpie.None[T]()`: + +```go +mock.Setup(printer.Printf("Hello world", kelpie.None[any]())) +mock.Called(secrets.Get(kelpie.Any[context.Context](), kelpie.Any[string](), kelpie.None[any]())) +``` + +The reason for using `None` is that otherwise the Go compiler can't infer the type of the variadic parameter: + +```go +// Fails with "cannot infer P1" +mock.Setup(printer.Printf("Nothing to say").Return("Nothing to say")) +``` + +Another option instead of using `None` is to specify the type arguments explicitly, but that can become very verbose, especially when using Kelpie's matching functions: + +```go +secretsManagerMock.Called( + secretsmanagerapi.PutSecretValue[mocking.Matcher[context.Context], mocking.Matcher[*secretsmanager.PutSecretValueInput], func(*secretsmanager.Options)]( + kelpie.Any[context.Context](), kelpie.Any[*secretsmanager.PutSecretValueInput]())) +``` + ### 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: diff --git a/examples/variadic_functions_test.go b/examples/variadic_functions_test.go index 4ca13f7..0ed972e 100644 --- a/examples/variadic_functions_test.go +++ b/examples/variadic_functions_test.go @@ -48,6 +48,25 @@ func (t *VariadicFunctionsTests) Test_Parameters_AnyMatch() { t.Equal("Hello Dolly. This is Louis, Dolly.", result) } +func (t *VariadicFunctionsTests) Test_Parameters_NoneProvided() { + // Arrange + mock := printer.NewMock() + + mock.Setup(printer.Printf("Nothing to say", kelpie.None[any]()). + Return("Nothing to say")) + + // Act + result1 := mock.Instance().Printf("Nothing to say") + result2 := mock.Instance().Printf("Who are %s", "you") + + // Assert + t.Equal("Nothing to say", result1) + t.Equal("", result2) + t.True(mock.Called(printer.Printf("Nothing to say", kelpie.None[any]()))) + t.True(mock.Called(printer.Printf(kelpie.Any[string](), kelpie.None[any]()))) + t.False(mock.Called(printer.Printf("Testing %d %d %d", 1, 2, 3))) +} + func (t *VariadicFunctionsTests) Test_Parameters_When() { // Arrange mock := printer.NewMock() diff --git a/kelpie.go b/kelpie.go index 10245e0..c3d3c06 100644 --- a/kelpie.go +++ b/kelpie.go @@ -23,3 +23,9 @@ func Any[T any]() mocking.Matcher[T] { func Match[T any](isMatch func(arg T) bool) mocking.Matcher[T] { return mocking.Matcher[T]{MatchFn: isMatch} } + +// 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]())). +func None[T any]() mocking.Matcher[T] { + return mocking.None[T]() +} diff --git a/mocking/matcher.go b/mocking/matcher.go index 186d660..707dd79 100644 --- a/mocking/matcher.go +++ b/mocking/matcher.go @@ -6,11 +6,16 @@ import "reflect" 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 } // Matcher is used to match an argument in a method invocation. type Matcher[T any] struct { - MatchFn func(input T) bool + MatchFn func(input T) bool + isNoneMatcher bool } // IsMatch returns true if other is a match to the expectation. @@ -18,6 +23,11 @@ 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 +} + type variadicMatcher struct { matchers []ArgumentMatcher } @@ -28,6 +38,11 @@ func Variadic(matchers []ArgumentMatcher) ArgumentMatcher { return &variadicMatcher{matchers: matchers} } +// IsNoneMatcher always returns false for variadicMatcher. +func (v *variadicMatcher) IsNoneMatcher() bool { + return false +} + // IsMatch returns true if other is a match to the expectation. func (v *variadicMatcher) IsMatch(other any) bool { args, ok := other.([]any) @@ -48,6 +63,10 @@ func (v *variadicMatcher) IsMatch(other any) bool { } } + if len(v.matchers) == 1 && v.matchers[0].IsNoneMatcher() { + return len(args) == 0 + } + if len(args) != len(v.matchers) { return false } @@ -61,6 +80,13 @@ func (v *variadicMatcher) IsMatch(other any) bool { return true } +// 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, + } +} + // MethodMatcher is used to match a method call to an expectation. type MethodMatcher struct { MethodName string diff --git a/mocking/matcher_test.go b/mocking/matcher_test.go index a30d7ca..1671ba9 100644 --- a/mocking/matcher_test.go +++ b/mocking/matcher_test.go @@ -35,6 +35,16 @@ func (t *MatcherTests) Test_VariadicMatcher_MatchesWhenParametersEmpty() { inputs: []any{}, isMatch: true, }, + "None matcher matches empty input": { + matchers: []mocking.ArgumentMatcher{mocking.None[any]()}, + inputs: []any{}, + isMatch: true, + }, + "None matcher does not match non-empty input": { + matchers: []mocking.ArgumentMatcher{mocking.None[any]()}, + inputs: []any{"testing"}, + isMatch: false, + }, "Matchers empty but arguments provided": { matchers: []mocking.ArgumentMatcher{}, inputs: []any{"testing", 1, 2, 3},