moq/pkg/moq/moq_test.go
Suhas Karanth 2ae606f132
Internal registry for disambiguated imports, vars (#141)
* Internal registry for disambiguated imports, vars
- Move functionality in the moq package partially into
  internal/{registry,template}.
- Leverage registry to assign unique package and variable/method
  parameter names. Use import aliases if present in interface source
  package.
BREAKING CHANGE: When the interface definition does not mention the
parameter names, the field names in call info anonymous struct will be
different.
The new field names are generated using the type info (string -> s,
int -> n, chan int -> intCh, []MyType -> myTypes, map[string]int ->
stringToInt etc.).
For example, for a string parameter previously if the field name was
'In1', the new field could be 'S' or 'S1' (depends on number of
string method parameters).
* Refactor golden file tests to be table-driven
* Fix sync pkg alias handling for moq generation
* Improve, add tests (increase coverage)
* Use $.Foo in template, avoid declaring variables
$ is set to the data argument passed to Execute, that is, to the
starting value of dot.
Variables were declared to be able to refer to the parent context.
* Consistent template field formatting
* Use tabs in generated Godoc comments' example code
* Minor simplification
* go generate
* Fix conflict for generated param name of pointer type

Excellent work by @sudo-suhas.
2021-02-01 19:20:20 +00:00

681 lines
19 KiB
Go

package moq
import (
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/pmezard/go-difflib/difflib"
)
var update = flag.Bool("update", false, "Update golden files.")
func TestMoq(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/example"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "PersonStore")
if err != nil {
t.Errorf("m.Mock: %s", err)
}
s := buf.String()
// assertions of things that should be mentioned
var strs = []string{
"package example",
"type PersonStoreMock struct",
"CreateFunc func(ctx context.Context, person *Person, confirm bool) error",
"GetFunc func(ctx context.Context, id string) (*Person, error)",
"func (mock *PersonStoreMock) Create(ctx context.Context, person *Person, confirm bool) error",
"func (mock *PersonStoreMock) Get(ctx context.Context, id string) (*Person, error)",
"panic(\"PersonStoreMock.CreateFunc: method is nil but PersonStore.Create was just called\")",
"panic(\"PersonStoreMock.GetFunc: method is nil but PersonStore.Get was just called\")",
"mock.lockGet.Lock()",
"mock.calls.Get = append(mock.calls.Get, callInfo)",
"mock.lockGet.Unlock()",
"// ID is the id argument value",
}
for _, str := range strs {
if !strings.Contains(s, str) {
t.Errorf("expected but missing: \"%s\"", str)
}
}
}
func TestMoqWithStaticCheck(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/example"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "PersonStore")
if err != nil {
t.Errorf("m.Mock: %s", err)
}
s := buf.String()
// assertions of things that should be mentioned
var strs = []string{
"package example",
"var _ PersonStore = &PersonStoreMock{}",
"type PersonStoreMock struct",
"CreateFunc func(ctx context.Context, person *Person, confirm bool) error",
"GetFunc func(ctx context.Context, id string) (*Person, error)",
"func (mock *PersonStoreMock) Create(ctx context.Context, person *Person, confirm bool) error",
"func (mock *PersonStoreMock) Get(ctx context.Context, id string) (*Person, error)",
"panic(\"PersonStoreMock.CreateFunc: method is nil but PersonStore.Create was just called\")",
"panic(\"PersonStoreMock.GetFunc: method is nil but PersonStore.Get was just called\")",
"mock.lockGet.Lock()",
"mock.calls.Get = append(mock.calls.Get, callInfo)",
"mock.lockGet.Unlock()",
"// ID is the id argument value",
}
for _, str := range strs {
if !strings.Contains(s, str) {
t.Errorf("expected but missing: \"%s\"", str)
}
}
}
func TestMoqWithAlias(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/example"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "PersonStore:AnotherPersonStoreMock")
if err != nil {
t.Errorf("m.Mock: %s", err)
}
s := buf.String()
// assertions of things that should be mentioned
var strs = []string{
"package example",
"type AnotherPersonStoreMock struct",
"CreateFunc func(ctx context.Context, person *Person, confirm bool) error",
"GetFunc func(ctx context.Context, id string) (*Person, error)",
"func (mock *AnotherPersonStoreMock) Create(ctx context.Context, person *Person, confirm bool) error",
"func (mock *AnotherPersonStoreMock) Get(ctx context.Context, id string) (*Person, error)",
"panic(\"AnotherPersonStoreMock.CreateFunc: method is nil but PersonStore.Create was just called\")",
"panic(\"AnotherPersonStoreMock.GetFunc: method is nil but PersonStore.Get was just called\")",
"mock.lockGet.Lock()",
"mock.calls.Get = append(mock.calls.Get, callInfo)",
"mock.lockGet.Unlock()",
"// ID is the id argument value",
}
for _, str := range strs {
if !strings.Contains(s, str) {
t.Errorf("expected but missing: \"%s\"", str)
}
}
}
func TestMoqExplicitPackage(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/example", PkgName: "different"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "PersonStore")
if err != nil {
t.Errorf("m.Mock: %s", err)
}
s := buf.String()
// assertions of things that should be mentioned
var strs = []string{
"package different",
"type PersonStoreMock struct",
"CreateFunc func(ctx context.Context, person *example.Person, confirm bool) error",
"GetFunc func(ctx context.Context, id string) (*example.Person, error)",
"func (mock *PersonStoreMock) Create(ctx context.Context, person *example.Person, confirm bool) error",
"func (mock *PersonStoreMock) Get(ctx context.Context, id string) (*example.Person, error)",
}
for _, str := range strs {
if !strings.Contains(s, str) {
t.Errorf("expected but missing: \"%s\"", str)
}
}
}
func TestMoqExplicitPackageWithStaticCheck(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/example", PkgName: "different"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "PersonStore")
if err != nil {
t.Errorf("m.Mock: %s", err)
}
s := buf.String()
// assertions of things that should be mentioned
var strs = []string{
"package different",
"var _ example.PersonStore = &PersonStoreMock{}",
"type PersonStoreMock struct",
"CreateFunc func(ctx context.Context, person *example.Person, confirm bool) error",
"GetFunc func(ctx context.Context, id string) (*example.Person, error)",
"func (mock *PersonStoreMock) Create(ctx context.Context, person *example.Person, confirm bool) error",
"func (mock *PersonStoreMock) Get(ctx context.Context, id string) (*example.Person, error)",
}
for _, str := range strs {
if !strings.Contains(s, str) {
t.Errorf("expected but missing: \"%s\"", str)
}
}
}
func TestMoqSkipEnsure(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/example", PkgName: "different", SkipEnsure: true})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "PersonStore")
if err != nil {
t.Errorf("m.Mock: %s", err)
}
s := buf.String()
// assertions of things that should be mentioned
var strs = []string{
"package different",
"type PersonStoreMock struct",
"CreateFunc func(ctx context.Context, person *example.Person, confirm bool) error",
"GetFunc func(ctx context.Context, id string) (*example.Person, error)",
"func (mock *PersonStoreMock) Create(ctx context.Context, person *example.Person, confirm bool) error",
"func (mock *PersonStoreMock) Get(ctx context.Context, id string) (*example.Person, error)",
}
for _, str := range strs {
if !strings.Contains(s, str) {
t.Errorf("expected but missing: \"%s\"", str)
}
}
}
func TestNotCreatingEmptyDirWhenPkgIsGiven(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/example", PkgName: "different"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "PersonStore")
if err != nil {
t.Errorf("m.Mock: %s", err)
}
s := buf.String()
if len(s) == 0 {
t.Fatalf("mock should be generated")
}
if _, err := os.Stat("testpackages/example/different"); !os.IsNotExist(err) {
t.Fatalf("no empty dir should be created by moq")
}
}
// TestVariadicArguments tests to ensure variadic work as
// expected.
// see https://github.com/matryer/moq/issues/5
func TestVariadicArguments(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/variadic"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "Greeter")
if err != nil {
t.Errorf("m.Mock: %s", err)
}
s := buf.String()
// assertions of things that should be mentioned
var strs = []string{
"package variadic",
"type GreeterMock struct",
"GreetFunc func(ctx context.Context, names ...string) string",
"return mock.GreetFunc(ctx, names...)",
}
for _, str := range strs {
if !strings.Contains(s, str) {
t.Errorf("expected but missing: \"%s\"", str)
}
}
}
func TestNothingToReturn(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/example"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "PersonStore")
if err != nil {
t.Errorf("m.Mock: %s", err)
}
s := buf.String()
if strings.Contains(s, `return mock.ClearCacheFunc(id)`) {
t.Errorf("should not have return for items that have no return arguments")
}
// assertions of things that should be mentioned
var strs = []string{
"mock.ClearCacheFunc(id)",
}
for _, str := range strs {
if !strings.Contains(s, str) {
t.Errorf("expected but missing: \"%s\"", str)
}
}
}
func TestImports(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/imports/two"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "DoSomething")
if err != nil {
t.Errorf("m.Mock: %s", err)
}
s := buf.String()
var strs = []string{
` "sync"`,
` "github.com/matryer/moq/pkg/moq/testpackages/imports/one"`,
}
for _, str := range strs {
if !strings.Contains(s, str) {
t.Errorf("expected but missing: \"%s\"", str)
}
if len(strings.Split(s, str)) > 2 {
t.Errorf("more than one: \"%s\"", str)
}
}
}
func TestMockGolden(t *testing.T) {
cases := []struct {
name string
cfg Config
interfaces []string
goldenFile string
}{
{
// Tests to ensure slice return data type works as expected.
// See https://github.com/matryer/moq/issues/124
name: "SliceResult",
cfg: Config{SrcDir: "testpackages/variadic"},
interfaces: []string{"Echoer"},
goldenFile: filepath.Join("testpackages/variadic", "echoer.golden.go"),
},
{
// Tests generation of mock where a method on the interface uses a
// blank identifier.
// See https://github.com/matryer/moq/issues/70
name: "BlankID",
cfg: Config{SrcDir: "testpackages/blankid"},
interfaces: []string{"Swallower"},
goldenFile: filepath.Join("testpackages/blankid", "swallower.golden.go"),
},
{
name: "ChannelNames",
cfg: Config{SrcDir: "testpackages/channels", StubImpl: true},
interfaces: []string{"Queuer"},
goldenFile: filepath.Join("testpackages/channels", "queuer_moq.golden.go"),
},
{
// Tests generation of mock when the interface imports a different
// package by the same name as it's own.
// See https://github.com/matryer/moq/issues/94
name: "PkgShadow",
cfg: Config{SrcDir: "testpackages/shadow/http", PkgName: "mock"},
interfaces: []string{"Thing"},
goldenFile: filepath.Join("testpackages/shadow/mock", "thing_moq.golden.go"),
},
{
// Tests generation of mock when a method parameter shadows an
// imported package name.
name: "ParamShadow",
cfg: Config{SrcDir: "testpackages/shadow"},
interfaces: []string{"Shadower"},
goldenFile: filepath.Join("testpackages/shadow", "shadower_moq.golden.go"),
},
{
name: "ImportAlias",
cfg: Config{SrcDir: "testpackages/importalias"},
interfaces: []string{"MiddleMan"},
goldenFile: filepath.Join("testpackages/importalias", "middleman_moq.golden.go"),
},
{
// Tests conflict resolution for generated names of method
// parameters.
name: "ParamNameConflict",
cfg: Config{SrcDir: "testpackages/paramconflict"},
interfaces: []string{"Interface"},
goldenFile: filepath.Join("testpackages/paramconflict", "iface_moq.golden.go"),
},
{
// Tests generation of names for unnamed method parameters.
name: "GenerateParamNames",
cfg: Config{SrcDir: "testpackages/genparamname"},
interfaces: []string{"Interface"},
goldenFile: filepath.Join("testpackages/genparamname", "iface_moq.golden.go"),
},
{
name: "SyncImport",
cfg: Config{SrcDir: "testpackages/syncimport"},
interfaces: []string{"Syncer"},
goldenFile: filepath.Join("testpackages/syncimport", "syncer_moq.golden.go"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
m, err := New(tc.cfg)
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
if err = m.Mock(&buf, tc.interfaces...); err != nil {
t.Errorf("m.Mock: %s", err)
return
}
if err := matchGoldenFile(tc.goldenFile, buf.Bytes()); err != nil {
t.Errorf("check golden file: %s", err)
}
})
}
}
func TestFormatter(t *testing.T) {
cases := []struct {
name string
conf Config
}{
{name: "gofmt", conf: Config{SrcDir: "testpackages/imports/two"}},
{name: "goimports", conf: Config{SrcDir: "testpackages/imports/two", Formatter: "goimports"}},
{name: "noop", conf: Config{SrcDir: "testpackages/imports/two", Formatter: "noop"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
m, err := New(tc.conf)
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "DoSomething:"+tc.name+"Mock")
if err != nil {
t.Errorf("m.Mock: %s", err)
}
golden := filepath.Join("testpackages/imports/two", tc.name+".golden.go")
if err := matchGoldenFile(golden, buf.Bytes()); err != nil {
t.Errorf("check golden file: %s", err)
}
})
}
}
func matchGoldenFile(goldenFile string, actual []byte) error {
// To update golden files, run the following:
// go test -v -run '^<Test-Name>$' github.com/matryer/moq/pkg/moq -update
if *update {
if err := os.MkdirAll(filepath.Dir(goldenFile), 0750); err != nil {
return fmt.Errorf("create dir: %s", err)
}
if err := ioutil.WriteFile(goldenFile, actual, 0600); err != nil {
return fmt.Errorf("write: %s", err)
}
return nil
}
expected, err := ioutil.ReadFile(goldenFile)
if err != nil {
return fmt.Errorf("read: %s: %s", goldenFile, err)
}
// Normalise newlines
actual, expected = normalize(actual), normalize(expected)
if !bytes.Equal(expected, actual) {
diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
A: difflib.SplitLines(string(expected)),
B: difflib.SplitLines(string(actual)),
FromFile: "Expected",
ToFile: "Actual",
Context: 1,
})
if err != nil {
return fmt.Errorf("diff: %s", err)
}
return fmt.Errorf("match: %s:\n%s", goldenFile, diff)
}
return nil
}
func TestVendoredPackages(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/vendoring/user"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "Service")
if err != nil {
t.Errorf("mock error: %s", err)
}
s := buf.String()
// assertions of things that should be mentioned
var strs = []string{
`"github.com/sudo-suhas/moq-test-pkgs/somerepo"`,
}
for _, str := range strs {
if !strings.Contains(s, str) {
t.Errorf("expected but missing: \"%s\"", str)
}
}
}
func TestVendoredInterface(t *testing.T) {
m, err := New(Config{
SrcDir: "testpackages/vendoring/vendor/github.com/sudo-suhas/moq-test-pkgs/somerepo",
PkgName: "someother",
})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "SomeService")
if err != nil {
t.Errorf("mock error: %s", err)
}
s := buf.String()
// assertions of things that should be mentioned
var strs = []string{
`"github.com/sudo-suhas/moq-test-pkgs/somerepo"`,
}
for _, str := range strs {
if !strings.Contains(s, str) {
t.Errorf("expected but missing: \"%s\"", str)
}
}
incorrectImport := `"github.com/matryer/moq/pkg/moq/testpackages/vendoring/vendor/github.com/sudo-suhas/moq-test-pkgs/somerepo"`
if strings.Contains(s, incorrectImport) {
t.Errorf("unexpected import: %s", incorrectImport)
}
}
func TestVendoredBuildConstraints(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/buildconstraints/user"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "Service")
if err != nil {
t.Errorf("mock error: %s", err)
}
s := buf.String()
// assertions of things that should be mentioned
var strs = []string{
`"github.com/sudo-suhas/moq-test-pkgs/buildconstraints"`,
}
for _, str := range strs {
if !strings.Contains(s, str) {
t.Errorf("expected but missing: \"%s\"", str)
}
}
}
// TestDotImports tests for https://github.com/matryer/moq/issues/21.
func TestDotImports(t *testing.T) {
preDir, err := os.Getwd()
if err != nil {
t.Errorf("Getwd: %s", err)
}
err = os.Chdir("testpackages/dotimport")
if err != nil {
t.Errorf("Chdir: %s", err)
}
defer func() {
err := os.Chdir(preDir)
if err != nil {
t.Errorf("Chdir back: %s", err)
}
}()
m, err := New(Config{SrcDir: ".", PkgName: "moqtest_test"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "Service")
if err != nil {
t.Errorf("mock error: %s", err)
}
s := buf.String()
if strings.Contains(s, `"."`) {
t.Error("contains invalid dot import")
}
}
func TestEmptyInterface(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/emptyinterface"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "Empty")
if err != nil {
t.Errorf("mock error: %s", err)
}
s := buf.String()
if strings.Contains(s, `"sync"`) {
t.Error("contains sync import, although this package isn't used")
}
}
func TestGoGenerateVendoredPackages(t *testing.T) {
cmd := exec.Command("go", "generate", "./...")
cmd.Dir = "testpackages/gogenvendoring"
stdout, err := cmd.StdoutPipe()
if err != nil {
t.Errorf("StdoutPipe: %s", err)
}
defer stdout.Close()
err = cmd.Start()
if err != nil {
t.Errorf("Start: %s", err)
}
buf := bytes.NewBuffer(nil)
io.Copy(buf, stdout)
err = cmd.Wait()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
t.Errorf("Wait: %s %s", exitErr, string(exitErr.Stderr))
} else {
t.Errorf("Wait: %s", err)
}
}
s := buf.String()
if strings.Contains(s, `vendor/`) {
t.Error("contains vendor directory in import path")
}
}
func TestImportedPackageWithSameName(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/samenameimport"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
var buf bytes.Buffer
err = m.Mock(&buf, "Example")
if err != nil {
t.Errorf("mock error: %s", err)
}
s := buf.String()
if !strings.Contains(s, `a samename.A`) {
t.Error("missing samename.A to address the struct A from the external package samename")
}
}
func TestParseError(t *testing.T) {
_, err := New(Config{SrcDir: "testpackages/_parseerror/service"})
if err == nil {
t.Errorf("expected error but got nil")
return
}
if !strings.Contains(err.Error(), `could not import github.com/matryer/notexist (invalid package name: "")`) {
t.Errorf("unexpected error: %s", err.Error())
}
}
func TestMockError(t *testing.T) {
m, err := New(Config{SrcDir: "testpackages/example"})
if err != nil {
t.Fatalf("moq.New: %s", err)
}
cases := []struct {
name string
namePair string
wantErr string
}{
{
name: "TypeNotFound",
namePair: "DoesNotExist",
wantErr: "interface not found: DoesNotExist",
},
{
name: "UnexpectedType",
namePair: "Person",
wantErr: "Person (github.com/matryer/moq/pkg/moq/testpackages/example.Person) is not an interface",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := m.Mock(ioutil.Discard, tc.namePair)
if err == nil {
t.Errorf("expected error but got nil")
return
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Errorf("unexpected error: %s", err.Error())
}
})
}
}
// normalize normalizes \r\n (windows) and \r (mac)
// into \n (unix)
func normalize(d []byte) []byte {
// Source: https://www.programming-books.io/essential/go/normalize-newlines-1d3abcf6f17c4186bb9617fa14074e48
// replace CR LF \r\n (windows) with LF \n (unix)
d = bytes.Replace(d, []byte{13, 10}, []byte{10}, -1)
// replace CF \r (mac) with LF \n (unix)
d = bytes.Replace(d, []byte{13}, []byte{10}, -1)
return d
}