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

feat(generator): add Expectation structs and functions #3

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
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
41 changes: 41 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,47 @@ requesterMock.EXPECT().
Note that the types of the arguments on the `EXPECT` methods are `interface{}`, not the actual type of your interface. The reason for this is that you may want to pass `mock.Any` as an argument, which means that the argument you pass may be an arbitrary type. The types are still provided in the expecter method docstrings.


Expectation Structs
-------------------

:octicons-tag-24: v2.26.0

Mockery now supports the generation of Expectation structs and functions. These structs and functions allow you to define expectations for your mock methods in a more structured way.

For example, given an interface such as
```go
type Expecter interface {
NoArg() string
NoReturn(str string)
ManyArgsReturns(str string, i int) (strs []string, err error)
Variadic(ints ...int) error
VariadicMany(i int, a string, intfs ...interface{}) error
}
```

You can define expectations using the generated Expectation structs and functions:

```go
expMock := mocks.Expecter{}

exp := mocks.ExpecterNoArgExpectation{
Args: mocks.ExpecterNoArgArgs{
StrAnything: true,
},
Returns: mocks.ExpecterNoArgReturns{
Str: "some string",
},
}
expMock.ApplyNoArgExpectation(exp)

str := expMock.NoArg()
require.Equal(t, "some string", str)
expMock.AssertExpectations(t)
```

The generated Expectation structs and functions provide a more structured way to define expectations for your mock methods, making your tests more readable and maintainable.


Return Value Providers
----------------------

Expand Down
3 changes: 0 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ require (
)

require (
github.com/frankban/quicktest v1.14.6 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand All @@ -37,11 +36,9 @@ require (
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/mod v0.23.0 // indirect
Expand Down
489 changes: 3 additions & 486 deletions go.sum

Large diffs are not rendered by default.

255 changes: 255 additions & 0 deletions go.work.sum

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions pkg/fixtures/expecter.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,78 @@ type Expecter interface {
type VariadicNoReturnInterface interface {
VariadicNoReturn(j int, is ...interface{})
}

type ExpecterNoArgArgs struct {
StrAnything bool
}

type ExpecterNoArgReturns struct {
Str string
}

type ExpecterNoArgExpectation struct {
Args ExpecterNoArgArgs
Returns ExpecterNoArgReturns
}

type ExpecterNoReturnArgs struct {
Str string
StrAnything bool
}

type ExpecterNoReturnReturns struct {
}

type ExpecterNoReturnExpectation struct {
Args ExpecterNoReturnArgs
Returns ExpecterNoReturnReturns
}

type ExpecterManyArgsReturnsArgs struct {
Str string
I int
StrAnything bool
IAnything bool
}

type ExpecterManyArgsReturnsReturns struct {
Strs []string
Err error
}

type ExpecterManyArgsReturnsExpectation struct {
Args ExpecterManyArgsReturnsArgs
Returns ExpecterManyArgsReturnsReturns
}

type ExpecterVariadicArgs struct {
Ints []int
IntsAnything bool
}

type ExpecterVariadicReturns struct {
Err error
}

type ExpecterVariadicExpectation struct {
Args ExpecterVariadicArgs
Returns ExpecterVariadicReturns
}

type ExpecterVariadicManyArgs struct {
I int
A string
Intfs []interface{}
IAnything bool
AAnything bool
IntfsAnything bool
}

type ExpecterVariadicManyReturns struct {
Err error
}

type ExpecterVariadicManyExpectation struct {
Args ExpecterVariadicManyArgs
Returns ExpecterVariadicManyReturns
}
85 changes: 85 additions & 0 deletions pkg/fixtures/test/expecter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,88 @@
panic("inftSlice only accepts slices or arrays")
}
}

func TestExpectationStructs(t *testing.T) {
expMock := mocks.Expecter{}

t.Run("NoArg", func(t *testing.T) {
exp := mocks.ExpecterNoArgExpectation{

Check failure on line 201 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.23)

undefined: mocks.ExpecterNoArgExpectation

Check failure on line 201 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.24)

undefined: mocks.ExpecterNoArgExpectation
Args: mocks.ExpecterNoArgArgs{

Check failure on line 202 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.23)

undefined: mocks.ExpecterNoArgArgs

Check failure on line 202 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.24)

undefined: mocks.ExpecterNoArgArgs
StrAnything: true,
},
Returns: mocks.ExpecterNoArgReturns{

Check failure on line 205 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.23)

undefined: mocks.ExpecterNoArgReturns

Check failure on line 205 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.24)

undefined: mocks.ExpecterNoArgReturns
Str: defaultString,
},
}
expMock.ApplyNoArgExpectation(exp)

Check failure on line 209 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.23)

expMock.ApplyNoArgExpectation undefined (type mocks.Expecter has no field or method ApplyNoArgExpectation)

Check failure on line 209 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.24)

expMock.ApplyNoArgExpectation undefined (type mocks.Expecter has no field or method ApplyNoArgExpectation)

str := expMock.NoArg()
require.Equal(t, defaultString, str)
expMock.AssertExpectations(t)
})

t.Run("NoReturn", func(t *testing.T) {
exp := mocks.ExpecterNoReturnExpectation{

Check failure on line 217 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.23)

undefined: mocks.ExpecterNoReturnExpectation

Check failure on line 217 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.24)

undefined: mocks.ExpecterNoReturnExpectation
Args: mocks.ExpecterNoReturnArgs{

Check failure on line 218 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.23)

undefined: mocks.ExpecterNoReturnArgs

Check failure on line 218 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.24)

undefined: mocks.ExpecterNoReturnArgs
Str: defaultString,
},
}
expMock.ApplyNoReturnExpectation(exp)

Check failure on line 222 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.23)

expMock.ApplyNoReturnExpectation undefined (type mocks.Expecter has no field or method ApplyNoReturnExpectation)

Check failure on line 222 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.24)

expMock.ApplyNoReturnExpectation undefined (type mocks.Expecter has no field or method ApplyNoReturnExpectation)

expMock.NoReturn(defaultString)
expMock.AssertExpectations(t)
})

t.Run("ManyArgsReturns", func(t *testing.T) {
exp := mocks.ExpecterManyArgsReturnsExpectation{

Check failure on line 229 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.23)

undefined: mocks.ExpecterManyArgsReturnsExpectation

Check failure on line 229 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.24)

undefined: mocks.ExpecterManyArgsReturnsExpectation
Args: mocks.ExpecterManyArgsReturnsArgs{

Check failure on line 230 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.23)

undefined: mocks.ExpecterManyArgsReturnsArgs

Check failure on line 230 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.24)

undefined: mocks.ExpecterManyArgsReturnsArgs
Str: defaultString,
I: defaultInt,
},
Returns: mocks.ExpecterManyArgsReturnsReturns{

Check failure on line 234 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.23)

undefined: mocks.ExpecterManyArgsReturnsReturns

Check failure on line 234 in pkg/fixtures/test/expecter_test.go

View workflow job for this annotation

GitHub Actions / test / test (ubuntu-latest, 1.24)

undefined: mocks.ExpecterManyArgsReturnsReturns
Strs: []string{defaultString, defaultString},
Err: defaultError,
},
}
expMock.ApplyManyArgsReturnsExpectation(exp)

strs, err := expMock.ManyArgsReturns(defaultString, defaultInt)
require.Equal(t, []string{defaultString, defaultString}, strs)
require.Equal(t, defaultError, err)
expMock.AssertExpectations(t)
})

t.Run("Variadic", func(t *testing.T) {
exp := mocks.ExpecterVariadicExpectation{
Args: mocks.ExpecterVariadicArgs{
Ints: []int{1, 2, 3},
},
Returns: mocks.ExpecterVariadicReturns{
Err: defaultError,
},
}
expMock.ApplyVariadicExpectation(exp)

err := expMock.Variadic(1, 2, 3)
require.Equal(t, defaultError, err)
expMock.AssertExpectations(t)
})

t.Run("VariadicMany", func(t *testing.T) {
exp := mocks.ExpecterVariadicManyExpectation{
Args: mocks.ExpecterVariadicManyArgs{
I: defaultInt,
A: defaultString,
Intfs: []interface{}{1, 2, 3},
},
Returns: mocks.ExpecterVariadicManyReturns{
Err: defaultError,
},
}
expMock.ApplyVariadicManyExpectation(exp)

err := expMock.VariadicMany(defaultInt, defaultString, 1, 2, 3)
require.Equal(t, defaultError, err)
expMock.AssertExpectations(t)
})
}
90 changes: 90 additions & 0 deletions pkg/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os"
"path/filepath"
"regexp"
"slices"
"sort"
"strings"
"text/template"
Expand Down Expand Up @@ -827,6 +828,8 @@ func (g *Generator) generateMethod(ctx context.Context, method *Method) {
returns := g.genList(ctx, ftype.Results(), false)
preamble, called := g.generateCalled(params, returns)

g.generateExpectation(fname, params, returns)

data := struct {
FunctionName string
Params *paramList
Expand Down Expand Up @@ -903,6 +906,93 @@ func (_m *{{.MockName}}{{.InstantiatedTypeString}}) {{.FunctionName}}({{join .Pa
}
}

func (g *Generator) generateExpectation(fname string, params, returns *paramList) {
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring repetitive logic in generateExpectation into helper functions to improve readability and testability.

The new generateExpectation function does add significant nested logic which may hurt maintainability. Consider refactoring repetitive tasks (such as cloning lists and capitalizing names) into dedicated helper functions. This reduces the function’s nesting and improves readability and testability.

For example, extract a helper for capitalization:

func capitalize(name string) string {
	if name == "" {
		return name
	}
	return strings.ToUpper(name[:1]) + name[1:]
}

Then update the loops:

for i, name := range params.Names {
	params.Names[i] = capitalize(name)
}
for i, name := range returns.Names {
	returns.Names[i] = capitalize(name)
}

Additionally, consider a helper to clone and adjust a parameter list if the same logic applies in multiple places:

func cloneAndCapitalize(pl *paramList) *paramList {
	return &paramList{
		Names: slices.Map(slices.Clone(pl.Names), capitalize),
		Types: slices.Clone(pl.Types),
	}
}

Now your function can be refactored to:

func (g *Generator) generateExpectation(fname string, params, returns *paramList) {
	params = cloneAndCapitalize(params)
	returns = cloneAndCapitalize(returns)

	data := struct {
		FunctionName  string
		InterfaceName string
		MockName      string
		Params        *paramList
		Returns       *paramList
	}{
		FunctionName:  fname,
		InterfaceName: g.iface.Name,
		MockName:      g.mockName(),
		Params:        params,
		Returns:       returns,
	}

	g.printTemplate(data, `
        ...large multiline template...
    `)
}

These changes reduce the complexity without altering functionality.

// clone slices to avoid shadow overwriting
params = &paramList{
Names: make([]string, 0),
Types: make([]string, 0),
}
if params != nil {
params.Names = slices.Clone(params.Names)
params.Types = slices.Clone(params.Types)
}
returns = &paramList{
Names: make([]string, 0),
Types: make([]string, 0),
}
if returns != nil {
returns.Names = slices.Clone(returns.Names)
returns.Types = slices.Clone(returns.Types)
}
for i, name := range params.Names {
params.Names[i] = strings.ToUpper(name[:1]) + name[1:]
}
for i, name := range returns.Names {
returns.Names[i] = strings.ToUpper(name[:1]) + name[1:]
}
data := struct {
FunctionName string
InterfaceName string
MockName string
Params *paramList
Returns *paramList
}{
FunctionName: fname,
InterfaceName: g.iface.Name,
MockName: g.mockName(),
Params: params,
Returns: returns,
}

g.printTemplate(data, `
{{- if .Params.Names }}
type {{ .InterfaceName }}{{ .FunctionName }}Args struct {
{{- range $i, $name := .Params.Names }}
{{ $name | firstUpper }} {{ trimPrefix (index $.Params.Types $i) "..."}}
{{ $name }}Anything bool
{{- end }}
}
{{ end }}

{{- if .Returns.Names }}
type {{ .InterfaceName }}{{ .FunctionName }}Returns struct {
{{- range $i, $name := .Returns.Names }}
{{ $name }} {{ index $.Returns.Types $i }}
{{- end }}
}
{{ end }}

type {{ .InterfaceName }}{{ .FunctionName }}Expectation struct {
{{- if .Params.Names }}
Args {{ .InterfaceName }}{{ .FunctionName }}Args
{{- end }}
{{- if .Returns.Names }}
Returns {{ .InterfaceName }}{{ .FunctionName }}Returns
{{- end }}
}

func (_m *{{ .MockName }}) Apply{{ .FunctionName }}Expectation(e {{ .InterfaceName }}{{ .FunctionName }}Expectation) {
var args []interface{}
{{- range $name := .Params.Names }}
if e.Args.{{ $name }}Anything {
args = append(args, mock.Anything)
} else {
args = append(args, e.Args.{{ $name }})
}
{{- end }}

_m.On("{{ .FunctionName }}", args...)
{{- if .Returns.Names -}}
.Return(
{{- range $i, $name := .Returns.Names -}}
e.Returns.{{ $name }},
{{- end }}
)
{{- end }}
}
`)
}

func (g *Generator) generateExpecterStruct(ctx context.Context) {
data := struct {
MockName, ExpecterName string
Expand Down
Loading