Skip to content

Commit

Permalink
Dedupe mocking peer methods during code generation when configured wi…
Browse files Browse the repository at this point in the history
…th multiple cloning methods
  • Loading branch information
kelveny committed Aug 2, 2024
1 parent b2b44ee commit 7d0acbe
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 20 deletions.
97 changes: 94 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ You can use multiple `-real` and `-mock` options to specify a set of real class
//go:generate mockcompose -n testFoo -c foo -real Foo,this:.:fmt
```

In the example, `mockcompose` will generate a `testFoo` class with `Foo()` method function be cloned from real foo class implementation, all callee functions (from package `.` and package `fmt`) and callee peer methods ( indicated by `this`) will be mocked.
In the example, `mockcompose` will generate a `testFoo` class with `Foo()` method function be cloned from real `foo` class implementation, all callee functions (from package `.` and package `fmt`) and callee peer methods (indicated by `this`) will be mocked.

source Go class code: `foo.go`

Expand Down Expand Up @@ -104,7 +104,7 @@ func (f *foo) Bar() bool {

```

`go generate` configuration: mocks.go
`go generate` configuration: `mocks.go`

```go
//go:generate mockcompose -n testFoo -c foo -real Foo,this:.:fmt
Expand Down Expand Up @@ -278,7 +278,7 @@ func TestFoo(t *testing.T) {
}
```

You can also apply the same approach to ordinary function:
You can apply the same approach to ordinary function:

```go
//go:generate mockcompose -n mockCallee -real functionThatUsesFunctionFromSameRoot,foo
Expand Down Expand Up @@ -375,6 +375,97 @@ func Test_functionThatUsesFunctionFromSameRoot(t *testing.T) {

```

Sometimes you may also want to test against a set of functions with their callee closure be mocked.

Source Go class in code: `bar.go`

```go
package bar

import (
"fmt"
"math/rand"
"time"
)

type fooBar struct {
name string
}

//go:generate mockcompose -n fooBarMock -c fooBar -real FooBar,this -real BarFoo,this:.

func (f *fooBar) FooBar() string {
if f.order()%2 == 0 {
fmt.Printf("ordinal order\n")

return fmt.Sprintf("%s: %s%s", f.name, f.Foo(), f.Bar())
}

fmt.Printf("reverse order\n")
return fmt.Sprintf("%s: %s%s", f.name, f.Bar(), f.Foo())
}

func (f *fooBar) BarFoo() string {
if order()%2 == 0 {
fmt.Printf("ordinal order\n")

return fmt.Sprintf("%s: %s%s", f.name, f.Bar(), f.Foo())
}

fmt.Printf("reverse order\n")
return fmt.Sprintf("%s: %s%s", f.name, f.Foo(), f.Bar())
}

func (f *fooBar) Foo() string {
return "Foo"
}

func (f *fooBar) Bar() string {
return "Bar"
}

func (f *fooBar) order() int {
rand.Seed(time.Now().UnixNano())
return rand.Int()
}

func order() int {
rand.Seed(time.Now().UnixNano())
return rand.Int()
}
```

Test on a set of method functions:

```go
func TestFooBar(t *testing.T) {
assert := require.New(t)

fb := &fooBarMock{
fooBar: fooBar{name: "TestFooBar"},
}

fb.On("order").Return(1).Once()
fb.On("order").Return(2).Once()

fb.mock_fooBarMock_BarFoo_bar.On("order").Return(2).Once()
fb.mock_fooBarMock_BarFoo_bar.On("order").Return(1).Once()

fb.On("Foo").Return("FooMocked")
fb.On("Bar").Return("BarMocked")

s1 := fb.FooBar()
assert.Equal("TestFooBar: BarMockedFooMocked", s1)
s2 := fb.BarFoo()
assert.Equal(s1, s2)

s1 = fb.FooBar()
assert.Equal("TestFooBar: FooMockedBarMocked", s1)
s2 = fb.BarFoo()
assert.Equal(s1, s2)
}
```

## FAQ

### 1. Can mockcompose generate mocked implementation for interfaces?
Expand Down
46 changes: 29 additions & 17 deletions cmd/clzgenerator.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,27 +195,30 @@ func (g *classMethodGenerator) getAutoMockCalleeConfig(
}

func (g *classMethodGenerator) composeMock(
composeContext map[string]any,
writer io.Writer,
fset *token.FileSet,
fnSpec *ast.FuncDecl,
) {
gogen.MockFunc(
writer,
g.mockPkgName,
g.mockName,
fset,
fnSpec.Name.Name,
fnSpec.Type.Params,
fnSpec.Type.Results,
nil,
)
if _, ok := composeContext[fnSpec.Name.Name]; !ok {
gogen.MockFunc(
writer,
g.mockPkgName,
g.mockName,
fset,
fnSpec.Name.Name,
fnSpec.Type.Params,
fnSpec.Type.Results,
nil,
)
composeContext[fnSpec.Name.Name] = struct{}{}
}
}

func (g *classMethodGenerator) generate(
writer io.Writer,
file *ast.File,
) error {

var buf bytes.Buffer

fset := token.NewFileSet()
Expand Down Expand Up @@ -263,6 +266,9 @@ func (g *classMethodGenerator) generateInternal(
) (generated bool, autoMockPkgs []string) {
writer.Write([]byte(fmt.Sprintf("package %s\n\n", g.mockPkgName)))

composeContext := map[string]any{}
imports := gosyntax.GetFileImportsAsMap(file)

if len(file.Decls) > 0 {
for _, d := range file.Decls {
if fnSpec, ok := d.(*ast.FuncDecl); ok {
Expand All @@ -275,9 +281,12 @@ func (g *classMethodGenerator) generateInternal(
// check if we need to clone a method function or a ordinary function
receiverSpec := gosyntax.FuncDeclReceiverSpec(fset, fnSpec)
if receiverSpec != nil {
//
// clone a method function
//

// find out callee situation
clzMethods := gosyntax.FindClassMethods(receiverSpec.TypeDecl, fset, file)
imports := gosyntax.GetFileImportsAsMap(file)
v := gosyntax.NewCalleeVisitor(
imports,
clzMethods,
Expand Down Expand Up @@ -305,7 +314,7 @@ func (g *classMethodGenerator) generateInternal(
autoMockPeer, pkgs := g.getAutoMockCalleeConfig(fnSpec.Name.Name)
if autoMockPeer {
// peer callee in order of how it is declared in file
g.generateMethodPeerCallees(writer, fset, file, fnSpec, v)
g.generateMethodPeerCallees(composeContext, writer, fset, file, fnSpec, v)
}

if len(pkgs) > 0 {
Expand All @@ -316,7 +325,9 @@ func (g *classMethodGenerator) generateInternal(
}
}
} else {
imports := gosyntax.GetFileImportsAsMap(file)
//
// clone a matched function
//
v := gosyntax.NewCalleeVisitor(
imports,
nil,
Expand Down Expand Up @@ -351,7 +362,7 @@ func (g *classMethodGenerator) generateInternal(
}
} else if matchType == MATCH_MOCK {
// generate mocked method
g.composeMock(writer, fset, fnSpec)
g.composeMock(composeContext, writer, fset, fnSpec)
}
} else {
// for any non-function declaration, export only imports
Expand All @@ -367,6 +378,7 @@ func (g *classMethodGenerator) generateInternal(
}

func (g *classMethodGenerator) generateMethodPeerCallees(
composeContext map[string]any,
writer io.Writer,
fset *token.FileSet,
file *ast.File,
Expand All @@ -382,7 +394,7 @@ func (g *classMethodGenerator) generateMethodPeerCallees(
gosyntax.ForEachFuncDeclInFile(file, func(fnSpec *ast.FuncDecl) {
if fnSpec.Name.Name == peerMethod &&
gosyntax.ReceiverDeclString(fset, callerFnSpec.Recv) == gosyntax.ReceiverDeclString(fset, fnSpec.Recv) {
g.composeMock(writer, fset, fnSpec)
g.composeMock(composeContext, writer, fset, fnSpec)
}
})
}
Expand Down Expand Up @@ -437,7 +449,7 @@ func (g *classMethodGenerator) getMockedPackageClzName(
pkgNameToMock string,
funcName string,
) string {
// scope mocked package class name at per-method per-package basis
// scope mocked package class name at per-caller per-ref-package basis
if pkgNameToMock != "." {
return fmt.Sprintf("mock_%s_%s_%s", g.mockName, funcName, pkgNameToMock)
}
Expand Down
53 changes: 53 additions & 0 deletions test/bar/bar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package bar

import (
"fmt"
"math/rand"
"time"
)

type fooBar struct {
name string
}

//go:generate mockcompose -n fooBarMock -c fooBar -real FooBar,this -real BarFoo,this:.

func (f *fooBar) FooBar() string {
if f.order()%2 == 0 {
fmt.Printf("ordinal order\n")

return fmt.Sprintf("%s: %s%s", f.name, f.Foo(), f.Bar())
}

fmt.Printf("reverse order\n")
return fmt.Sprintf("%s: %s%s", f.name, f.Bar(), f.Foo())
}

func (f *fooBar) BarFoo() string {
if order()%2 == 0 {
fmt.Printf("ordinal order\n")

return fmt.Sprintf("%s: %s%s", f.name, f.Bar(), f.Foo())
}

fmt.Printf("reverse order\n")
return fmt.Sprintf("%s: %s%s", f.name, f.Foo(), f.Bar())
}

func (f *fooBar) Foo() string {
return "Foo"
}

func (f *fooBar) Bar() string {
return "Bar"
}

func (f *fooBar) order() int {
rand.Seed(time.Now().UnixNano())
return rand.Int()
}

func order() int {
rand.Seed(time.Now().UnixNano())
return rand.Int()
}
34 changes: 34 additions & 0 deletions test/bar/bar_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package bar

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestFooBar(t *testing.T) {
assert := require.New(t)

fb := &fooBarMock{
fooBar: fooBar{name: "TestFooBar"},
}

fb.On("order").Return(1).Once()
fb.On("order").Return(2).Once()

fb.mock_fooBarMock_BarFoo_bar.On("order").Return(2).Once()
fb.mock_fooBarMock_BarFoo_bar.On("order").Return(1).Once()

fb.On("Foo").Return("FooMocked")
fb.On("Bar").Return("BarMocked")

s1 := fb.FooBar()
assert.Equal("TestFooBar: BarMockedFooMocked", s1)
s2 := fb.BarFoo()
assert.Equal(s1, s2)

s1 = fb.FooBar()
assert.Equal("TestFooBar: FooMockedBarMocked", s1)
s2 = fb.BarFoo()
assert.Equal(s1, s2)
}
Loading

0 comments on commit 7d0acbe

Please sign in to comment.