From 20f5588224022df997929034f7faa0a9c4f6a39d Mon Sep 17 00:00:00 2001 From: Tetsuro Aoki Date: Sun, 29 Sep 2024 02:32:32 +0000 Subject: [PATCH] Add support for embedding types in mocks --- README.md | 9 ++- internal/template/template.go | 7 ++ internal/template/template_data.go | 8 +++ main.go | 9 +++ pkg/moq/moq.go | 59 ++++++++++++--- pkg/moq/moq_test.go | 24 +++++++ pkg/moq/testpackages/embedtypes/embedtypes.go | 36 ++++++++++ .../mock/embed_multiple_types.golden.go | 71 +++++++++++++++++++ .../mock/embed_pointer_type.golden.go | 70 ++++++++++++++++++ .../embedtypes/mock/embed_type.golden.go | 70 ++++++++++++++++++ .../mock/embed_types_with_alias.golden.go | 71 +++++++++++++++++++ 11 files changed, 425 insertions(+), 9 deletions(-) create mode 100644 pkg/moq/testpackages/embedtypes/embedtypes.go create mode 100644 pkg/moq/testpackages/embedtypes/mock/embed_multiple_types.golden.go create mode 100644 pkg/moq/testpackages/embedtypes/mock/embed_pointer_type.golden.go create mode 100644 pkg/moq/testpackages/embedtypes/mock/embed_type.golden.go create mode 100644 pkg/moq/testpackages/embedtypes/mock/embed_types_with_alias.golden.go diff --git a/README.md b/README.md index f83a702..2f3ef71 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,15 @@ moq [flags] source-dir interface [interface2 [interface3 [...]]] generate functions to facilitate resetting calls made to a mock Specifying an alias for the mock is also supported with the format 'interface:alias' - Ex: moq -pkg different . MyInterface:MyMock + +To embed types into the mock, use the format 'interface{type1,type2...}' or 'interface:alias{type1,type2...}' +- The types must be declared in source package +- You can embed a pointer type by using the '*' prefix +Ex1: moq -pkg different src MyInterface{Type1} +Ex2: moq -pkg different src MyInterface{*Type1} +Ex3: moq -pkg different src MyInterface{Type1,Type2} +Ex4: moq -pkg different src MyInterface:MyMock{Type1} ``` **NOTE:** `source-dir` is the directory where the source code (definition) of the target interface is located. diff --git a/internal/template/template.go b/internal/template/template.go index 9e2672c..17625ac 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -89,6 +89,13 @@ type {{.MockName}} {{- if $index}}, {{end}}{{$param.Name | Exported}} {{$param.TypeString}} {{- end -}}] {{- end }} struct { +{{- range .EmbeddedTypes}} + {{if .IsPointer}}*{{end -}} + {{$.SrcPkgQualifier -}} + {{- .Name -}} +{{end}} +{{- if .EmbeddedTypes}} +{{end -}} {{- range .Methods}} // {{.Name}}Func mocks the {{.Name}} method. {{.Name}}Func func({{.ArgList}}) {{.ReturnArgTypeList}} diff --git a/internal/template/template_data.go b/internal/template/template_data.go index eb39fc9..02ef6cb 100644 --- a/internal/template/template_data.go +++ b/internal/template/template_data.go @@ -37,6 +37,7 @@ type MockData struct { MockName string TypeParams []TypeParamData Methods []MethodData + EmbeddedTypes []EmbeddedTypeData } // MethodData is the data which represents a method on some interface. @@ -132,3 +133,10 @@ func (p ParamData) CallName() string { func (p ParamData) TypeString() string { return p.Var.TypeString() } + +// EmbeddedTypeData is the data which represents a type will be embedded +// in the mock. +type EmbeddedTypeData struct { + Name string + IsPointer bool +} diff --git a/main.go b/main.go index e2a1fec..9d269e0 100644 --- a/main.go +++ b/main.go @@ -43,8 +43,17 @@ func main() { flag.Usage = func() { fmt.Println(`moq [flags] source-dir interface [interface2 [interface3 [...]]]`) flag.PrintDefaults() + fmt.Println() fmt.Println(`Specifying an alias for the mock is also supported with the format 'interface:alias'`) fmt.Println(`Ex: moq -pkg different . MyInterface:MyMock`) + fmt.Println() + fmt.Println(`To embed types into the mock, use the format 'interface{type1,type2...}' or 'interface:alias{type1,type2...}'`) + fmt.Println(`- The types must be declared in source package`) + fmt.Println(`- You can embed a pointer type by using the '*' prefix`) + fmt.Println(`Ex1: moq -pkg different src MyInterface{Type1}`) + fmt.Println(`Ex2: moq -pkg different src MyInterface{*Type1}`) + fmt.Println(`Ex3: moq -pkg different src MyInterface{Type1,Type2}`) + fmt.Println(`Ex4: moq -pkg different src MyInterface:MyMock{Type1}`) } flag.Parse() diff --git a/pkg/moq/moq.go b/pkg/moq/moq.go index e8a2975..93b75e5 100644 --- a/pkg/moq/moq.go +++ b/pkg/moq/moq.go @@ -6,6 +6,7 @@ import ( "go/token" "go/types" "io" + "regexp" "strings" "github.com/matryer/moq/internal/registry" @@ -51,29 +52,39 @@ func New(cfg Config) (*Mocker, error) { } // Mock generates a mock for the specified interface name. -func (m *Mocker) Mock(w io.Writer, namePairs ...string) error { - if len(namePairs) == 0 { +func (m *Mocker) Mock(w io.Writer, targets ...string) error { + if len(targets) == 0 { return errors.New("must specify one interface") } - mocks := make([]template.MockData, len(namePairs)) - for i, np := range namePairs { - name, mockName := parseInterfaceName(np) + differentPkg := m.registry.SrcPkgName() != m.mockPkgName() + + mocks := make([]template.MockData, len(targets)) + for i, target := range targets { + namePair, embeddedTypeList := parseTarget(target) + + name, mockName := parseInterfaceName(namePair) iface, tparams, err := m.registry.LookupInterface(name) if err != nil { return err } - methods := make([]template.MethodData, iface.NumMethods()) + methods := make([]template.MethodData, 0, iface.NumMethods()) for j := 0; j < iface.NumMethods(); j++ { - methods[j] = m.methodData(iface.Method(j)) + fn := iface.Method(j) + if !differentPkg || fn.Exported() { + methods = append(methods, m.methodData(fn)) + } } + embeddedTypes := parseEmbeddedTypes(embeddedTypeList) + mocks[i] = template.MockData{ InterfaceName: name, MockName: mockName, Methods: methods, TypeParams: m.typeParams(tparams), + EmbeddedTypes: embeddedTypes, } } @@ -88,7 +99,7 @@ func (m *Mocker) Mock(w io.Writer, namePairs ...string) error { if data.MocksSomeMethod() { m.registry.AddImport(types.NewPackage("sync", "sync")) } - if m.registry.SrcPkgName() != m.mockPkgName() { + if differentPkg { data.SrcPkgQualifier = m.registry.SrcPkgName() + "." if !m.cfg.SkipEnsure { imprt := m.registry.AddImport(m.registry.SrcPkg()) @@ -201,6 +212,18 @@ func (m *Mocker) format(src []byte) ([]byte, error) { return gofmt(src) } +var targetRegexp = regexp.MustCompile(`^(.+){(.+)}$`) + +func parseTarget(target string) (namePair, embeddedTypeList string) { + matched := targetRegexp.FindStringSubmatch(target) + + if len(matched) != 3 { + return target, "" + } + + return matched[1], matched[2] +} + func parseInterfaceName(namePair string) (ifaceName, mockName string) { parts := strings.SplitN(namePair, ":", 2) if len(parts) == 2 { @@ -210,3 +233,23 @@ func parseInterfaceName(namePair string) (ifaceName, mockName string) { ifaceName = parts[0] return ifaceName, ifaceName + "Mock" } + +func parseEmbeddedTypes(list string) []template.EmbeddedTypeData { + if list == "" { + return nil + } + + parts := strings.Split(list, ",") + + embeddedTypes := make([]template.EmbeddedTypeData, len(parts)) + for i, p := range parts { + isPointer := strings.HasPrefix(p, "*") + name := strings.TrimPrefix(p, "*") + embeddedTypes[i] = template.EmbeddedTypeData{ + Name: name, + IsPointer: isPointer, + } + } + + return embeddedTypes +} diff --git a/pkg/moq/moq_test.go b/pkg/moq/moq_test.go index 1e31cfb..d1f8c8e 100644 --- a/pkg/moq/moq_test.go +++ b/pkg/moq/moq_test.go @@ -418,6 +418,30 @@ func TestMockGolden(t *testing.T) { interfaces: []string{"Example"}, goldenFile: filepath.Join("testpackages/typealias", "typealias_moq.golden.go"), }, + { + name: "EmbedType", + cfg: Config{SrcDir: "testpackages/embedtypes", PkgName: "mock"}, + interfaces: []string{"Interface1{Embedded1}"}, + goldenFile: filepath.Join("testpackages/embedtypes/mock", "embed_type.golden.go"), + }, + { + name: "EmbedPointerType", + cfg: Config{SrcDir: "testpackages/embedtypes", PkgName: "mock"}, + interfaces: []string{"Interface2{*Embedded2}"}, + goldenFile: filepath.Join("testpackages/embedtypes/mock", "embed_pointer_type.golden.go"), + }, + { + name: "EmbedMultipleTypes", + cfg: Config{SrcDir: "testpackages/embedtypes", PkgName: "mock"}, + interfaces: []string{"Interface3{Embedded1,*Embedded2}"}, + goldenFile: filepath.Join("testpackages/embedtypes/mock", "embed_multiple_types.golden.go"), + }, + { + name: "EmbedTypesWithAlias", + cfg: Config{SrcDir: "testpackages/embedtypes", PkgName: "mock"}, + interfaces: []string{"Interface3:Alias{Embedded1,*Embedded2}"}, + goldenFile: filepath.Join("testpackages/embedtypes/mock", "embed_types_with_alias.golden.go"), + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/pkg/moq/testpackages/embedtypes/embedtypes.go b/pkg/moq/testpackages/embedtypes/embedtypes.go new file mode 100644 index 0000000..19e475f --- /dev/null +++ b/pkg/moq/testpackages/embedtypes/embedtypes.go @@ -0,0 +1,36 @@ +package embedtypes + +// Interface1 is a test interface. +// Its implementation must embed Embedded1. +type Interface1 interface { + ExportedMethod() + + unexportedMethod1() +} + +// Embedded1 has unexportedMethod1. +type Embedded1 struct{} + +func (e Embedded1) unexportedMethod1() {} + +// Interface2 is a test interface. +// Its implementation must embed Embedded2. +type Interface2 interface { + ExportedMethod() + + unexportedMethod2() +} + +// Embedded2 has unexportedMethod2. +type Embedded2 struct{} + +func (e *Embedded2) unexportedMethod2() {} + +// Interface3 is a test interface. +// Its implementation must embed Embedded1 and Embedded2. +type Interface3 interface { + ExportedMethod() + + unexportedMethod1() + unexportedMethod2() +} diff --git a/pkg/moq/testpackages/embedtypes/mock/embed_multiple_types.golden.go b/pkg/moq/testpackages/embedtypes/mock/embed_multiple_types.golden.go new file mode 100644 index 0000000..19a99f3 --- /dev/null +++ b/pkg/moq/testpackages/embedtypes/mock/embed_multiple_types.golden.go @@ -0,0 +1,71 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package mock + +import ( + "github.com/matryer/moq/pkg/moq/testpackages/embedtypes" + "sync" +) + +// Ensure, that Interface3Mock does implement embedtypes.Interface3. +// If this is not the case, regenerate this file with moq. +var _ embedtypes.Interface3 = &Interface3Mock{} + +// Interface3Mock is a mock implementation of embedtypes.Interface3. +// +// func TestSomethingThatUsesInterface3(t *testing.T) { +// +// // make and configure a mocked embedtypes.Interface3 +// mockedInterface3 := &Interface3Mock{ +// ExportedMethodFunc: func() { +// panic("mock out the ExportedMethod method") +// }, +// } +// +// // use mockedInterface3 in code that requires embedtypes.Interface3 +// // and then make assertions. +// +// } +type Interface3Mock struct { + embedtypes.Embedded1 + *embedtypes.Embedded2 + + // ExportedMethodFunc mocks the ExportedMethod method. + ExportedMethodFunc func() + + // calls tracks calls to the methods. + calls struct { + // ExportedMethod holds details about calls to the ExportedMethod method. + ExportedMethod []struct { + } + } + lockExportedMethod sync.RWMutex +} + +// ExportedMethod calls ExportedMethodFunc. +func (mock *Interface3Mock) ExportedMethod() { + if mock.ExportedMethodFunc == nil { + panic("Interface3Mock.ExportedMethodFunc: method is nil but Interface3.ExportedMethod was just called") + } + callInfo := struct { + }{} + mock.lockExportedMethod.Lock() + mock.calls.ExportedMethod = append(mock.calls.ExportedMethod, callInfo) + mock.lockExportedMethod.Unlock() + mock.ExportedMethodFunc() +} + +// ExportedMethodCalls gets all the calls that were made to ExportedMethod. +// Check the length with: +// +// len(mockedInterface3.ExportedMethodCalls()) +func (mock *Interface3Mock) ExportedMethodCalls() []struct { +} { + var calls []struct { + } + mock.lockExportedMethod.RLock() + calls = mock.calls.ExportedMethod + mock.lockExportedMethod.RUnlock() + return calls +} diff --git a/pkg/moq/testpackages/embedtypes/mock/embed_pointer_type.golden.go b/pkg/moq/testpackages/embedtypes/mock/embed_pointer_type.golden.go new file mode 100644 index 0000000..ebd7b07 --- /dev/null +++ b/pkg/moq/testpackages/embedtypes/mock/embed_pointer_type.golden.go @@ -0,0 +1,70 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package mock + +import ( + "github.com/matryer/moq/pkg/moq/testpackages/embedtypes" + "sync" +) + +// Ensure, that Interface2Mock does implement embedtypes.Interface2. +// If this is not the case, regenerate this file with moq. +var _ embedtypes.Interface2 = &Interface2Mock{} + +// Interface2Mock is a mock implementation of embedtypes.Interface2. +// +// func TestSomethingThatUsesInterface2(t *testing.T) { +// +// // make and configure a mocked embedtypes.Interface2 +// mockedInterface2 := &Interface2Mock{ +// ExportedMethodFunc: func() { +// panic("mock out the ExportedMethod method") +// }, +// } +// +// // use mockedInterface2 in code that requires embedtypes.Interface2 +// // and then make assertions. +// +// } +type Interface2Mock struct { + *embedtypes.Embedded2 + + // ExportedMethodFunc mocks the ExportedMethod method. + ExportedMethodFunc func() + + // calls tracks calls to the methods. + calls struct { + // ExportedMethod holds details about calls to the ExportedMethod method. + ExportedMethod []struct { + } + } + lockExportedMethod sync.RWMutex +} + +// ExportedMethod calls ExportedMethodFunc. +func (mock *Interface2Mock) ExportedMethod() { + if mock.ExportedMethodFunc == nil { + panic("Interface2Mock.ExportedMethodFunc: method is nil but Interface2.ExportedMethod was just called") + } + callInfo := struct { + }{} + mock.lockExportedMethod.Lock() + mock.calls.ExportedMethod = append(mock.calls.ExportedMethod, callInfo) + mock.lockExportedMethod.Unlock() + mock.ExportedMethodFunc() +} + +// ExportedMethodCalls gets all the calls that were made to ExportedMethod. +// Check the length with: +// +// len(mockedInterface2.ExportedMethodCalls()) +func (mock *Interface2Mock) ExportedMethodCalls() []struct { +} { + var calls []struct { + } + mock.lockExportedMethod.RLock() + calls = mock.calls.ExportedMethod + mock.lockExportedMethod.RUnlock() + return calls +} diff --git a/pkg/moq/testpackages/embedtypes/mock/embed_type.golden.go b/pkg/moq/testpackages/embedtypes/mock/embed_type.golden.go new file mode 100644 index 0000000..ab916e8 --- /dev/null +++ b/pkg/moq/testpackages/embedtypes/mock/embed_type.golden.go @@ -0,0 +1,70 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package mock + +import ( + "github.com/matryer/moq/pkg/moq/testpackages/embedtypes" + "sync" +) + +// Ensure, that Interface1Mock does implement embedtypes.Interface1. +// If this is not the case, regenerate this file with moq. +var _ embedtypes.Interface1 = &Interface1Mock{} + +// Interface1Mock is a mock implementation of embedtypes.Interface1. +// +// func TestSomethingThatUsesInterface1(t *testing.T) { +// +// // make and configure a mocked embedtypes.Interface1 +// mockedInterface1 := &Interface1Mock{ +// ExportedMethodFunc: func() { +// panic("mock out the ExportedMethod method") +// }, +// } +// +// // use mockedInterface1 in code that requires embedtypes.Interface1 +// // and then make assertions. +// +// } +type Interface1Mock struct { + embedtypes.Embedded1 + + // ExportedMethodFunc mocks the ExportedMethod method. + ExportedMethodFunc func() + + // calls tracks calls to the methods. + calls struct { + // ExportedMethod holds details about calls to the ExportedMethod method. + ExportedMethod []struct { + } + } + lockExportedMethod sync.RWMutex +} + +// ExportedMethod calls ExportedMethodFunc. +func (mock *Interface1Mock) ExportedMethod() { + if mock.ExportedMethodFunc == nil { + panic("Interface1Mock.ExportedMethodFunc: method is nil but Interface1.ExportedMethod was just called") + } + callInfo := struct { + }{} + mock.lockExportedMethod.Lock() + mock.calls.ExportedMethod = append(mock.calls.ExportedMethod, callInfo) + mock.lockExportedMethod.Unlock() + mock.ExportedMethodFunc() +} + +// ExportedMethodCalls gets all the calls that were made to ExportedMethod. +// Check the length with: +// +// len(mockedInterface1.ExportedMethodCalls()) +func (mock *Interface1Mock) ExportedMethodCalls() []struct { +} { + var calls []struct { + } + mock.lockExportedMethod.RLock() + calls = mock.calls.ExportedMethod + mock.lockExportedMethod.RUnlock() + return calls +} diff --git a/pkg/moq/testpackages/embedtypes/mock/embed_types_with_alias.golden.go b/pkg/moq/testpackages/embedtypes/mock/embed_types_with_alias.golden.go new file mode 100644 index 0000000..16473e4 --- /dev/null +++ b/pkg/moq/testpackages/embedtypes/mock/embed_types_with_alias.golden.go @@ -0,0 +1,71 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package mock + +import ( + "github.com/matryer/moq/pkg/moq/testpackages/embedtypes" + "sync" +) + +// Ensure, that Alias does implement embedtypes.Interface3. +// If this is not the case, regenerate this file with moq. +var _ embedtypes.Interface3 = &Alias{} + +// Alias is a mock implementation of embedtypes.Interface3. +// +// func TestSomethingThatUsesInterface3(t *testing.T) { +// +// // make and configure a mocked embedtypes.Interface3 +// mockedInterface3 := &Alias{ +// ExportedMethodFunc: func() { +// panic("mock out the ExportedMethod method") +// }, +// } +// +// // use mockedInterface3 in code that requires embedtypes.Interface3 +// // and then make assertions. +// +// } +type Alias struct { + embedtypes.Embedded1 + *embedtypes.Embedded2 + + // ExportedMethodFunc mocks the ExportedMethod method. + ExportedMethodFunc func() + + // calls tracks calls to the methods. + calls struct { + // ExportedMethod holds details about calls to the ExportedMethod method. + ExportedMethod []struct { + } + } + lockExportedMethod sync.RWMutex +} + +// ExportedMethod calls ExportedMethodFunc. +func (mock *Alias) ExportedMethod() { + if mock.ExportedMethodFunc == nil { + panic("Alias.ExportedMethodFunc: method is nil but Interface3.ExportedMethod was just called") + } + callInfo := struct { + }{} + mock.lockExportedMethod.Lock() + mock.calls.ExportedMethod = append(mock.calls.ExportedMethod, callInfo) + mock.lockExportedMethod.Unlock() + mock.ExportedMethodFunc() +} + +// ExportedMethodCalls gets all the calls that were made to ExportedMethod. +// Check the length with: +// +// len(mockedInterface3.ExportedMethodCalls()) +func (mock *Alias) ExportedMethodCalls() []struct { +} { + var calls []struct { + } + mock.lockExportedMethod.RLock() + calls = mock.calls.ExportedMethod + mock.lockExportedMethod.RUnlock() + return calls +}