genmock

package module
v0.0.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 8, 2023 License: MIT Imports: 6 Imported by: 1

README

genmock

Golang mocking system with code generator to check the types of the arguments at the compilation stage.

Features

  • Dependency-free in generated mock code
  • Generated code contains methods and typed arguments in builder style
  • No panic in the test code:
    • All test checkers calls t.Fatal on unexpected behaviour
    • Or it can to return the mock error as a result of the method if it possible
  • Flexible code generator, see config of the generator
  • Also you can use the package genmock without code generator

See examples here.

Install

go install gitlab.com/so_literate/genmock/cmd/genmock@latest

Code generator

// This interface:
type Iface interface {
	Method(i int64) error
}

// You can test like this:
iface := mock.NewIface_Mock(t)

iface.On_Method().Args(1).Rets(nil).Times(genmock.TimesAny)

// All methods Args and Rets have typed arguments:
//   method builder -> Args(i int64).Rets(_r0 error)
//
// Also, you have flexible methods to pass interface or custom matcher:
//   ArgsAnything(i interface{}) or Arg_i_Matcher(matcher func(i int64) bool)
//
// Using strongly typed methods ensures that changes are checked at compile time.
// You can use Go compilation power instead panic at runtime.

Using

1. Find your interface in your code and add magic comment
//go:generate genmock -search.name=SomeIface
type SomeIface interface {
	Method()
}

You can skip -search.name= falg if you have only one interface in the package.

2. Run go generate command
go generate ./...
3. Got your mocked interface here:
  • in package mock near of your interface
  • in file: ./mock/some_iface.go
  • with mock package name
4. Now you can import the mock package in your test files:
package pkg_test

import (
	"testing"
	
	"host/user/project/pkg/mock"
)

func TestIface(t *testing.T) {
	someIface := mock.NewSomeIface_Mock(t)
	
	someIface.On_Method().Times(1)
	
	if err := someIface.Mock.AssertExpectations(); err != nil {
		t.Fatal(err)
	}
}

This is a good way, if you follow the best practices for writing interfaces in go:

Keep interfaces small (the bigger the interface, the weaker abstraction)

You also have documentation in the code to generate the mock of the interface, not in Makefile or in another build script.

Generate test code in the package

You can generate mock code as your own test code of the package.

Add additional keys to genmock util:

//go:generate genmock -search.name=SomeIface -print.file.test -print.place.in_package -print.package.test

Now you got generated mock code in the package:

  • file in the package: some_iface_test.go
  • package name will be pkg_test

After that you can write your own test code using generated code without import of the mock package.

Generate interfaces over project

You can use the genmock to scan the entire project code.

Generator and Usage example:
genmock -search.recursive -print.place.keep_tree

Genmock will create directory ./mock with full import path of the each interface.

You can set some directories to skip in scan proccess -search.skip_paths=./vendor.

import "host/user/project/mock/host/user/project/pkg"

Config

You can configure the genmock in various ways.

1. Flags

Type genmock -h to see flag help message.

It contains 4 partitions:

  • global: version, timeout (1 minute), config, h, help
  • log: debug, no_color, pretty (true), quiet
  • search: root ('.'), name ('.*'), skip_paths, recursive, include_tests, include_not_types
  • print:
    • file: case (snake), test
    • place: path ('./mock'), in_package, keep_tree, stdout
    • package: name ('mock'), test
    • content: hide_version, tags

2. Environment variables

GENMOCK_LOG_DEBUG="false"
GENMOCK_LOG_QUIET="false"
GENMOCK_LOG_PRETTY="true"
GENMOCK_LOG_NO_COLOR="false"
GENMOCK_SEARCH_NAME=".*"
GENMOCK_SEARCH_ROOT="."
GENMOCK_SEARCH_RECURSIVE="false"
GENMOCK_SEARCH_SKIP_PATHS=""
GENMOCK_SEARCH_INCLUDE_TESTS="false"
GENMOCK_SEARCH_INCLUDE_NOT_TYPES="false"
GENMOCK_PRINT_FILE_CASE="snake"
GENMOCK_PRINT_FILE_TEST="false"
GENMOCK_PRINT_PLACE_PATH="./mock"
GENMOCK_PRINT_PLACE_KEEP_TREE="false"
GENMOCK_PRINT_PLACE_IN_PACKAGE="false"
GENMOCK_PRINT_PLACE_STDOUT="false"
GENMOCK_PRINT_PACKAGE_NAME="mock"
GENMOCK_PRINT_PACKAGE_TEST="false"
GENMOCK_PRINT_CONTENT_HIDE_VERSION="false"
GENMOCK_PRINT_CONTENT_TAGS=""
GENMOCK_TIMEOUT="1m"
GENMOCK_VERSION="false"

3. Config file

Pass you config file in -config flag genmock -config=/path/to/config.json

File example:

{
  "Log": {
    "Debug": false,
    "Quiet": false,
    "Pretty": true,
    "NoColor": false
  },
  "Search": {
    "Name": ".*",
    "Root": ".",
    "Recursive": false,
    "SkipPaths": [],
    "IncludeTests": false,
    "IncludeNotTypes": false
  },
  "Print": {
    "File": {
      "Case": "snake",
      "Test": false
    },
    "Place": {
      "Path": "./mock",
      "KeepTree": false,
      "InPackage": false,
      "Stdout": false
    },
    "Package": {
      "Name": "mock",
      "Test": false
    },
    "Content": {
      "HideVersion": false,
      "Tags": ""
    }
  },
  "Timeout": 60000000000,
  "Version": false
}

Documentation

Overview

Package genmock is a core of the mock system. You can use it as is (see ExampleMock) to mock any your object, but it is better to use the generator of the test objects. If you will be use generator you can handle errors of argument types in compile code stage instead panic in runtime. This mock returns error instead of panic, so you can handle error and stop you tests correctly.

Example

Example of using the generated code with mock interface.

package main

import (
	"fmt"
	"log"

	"gitlab.com/so_literate/genmock"
)

// Example of using the generated code with mock interface.
func main() {
	iface := NewIface_Mock(genmock.NewLoggerHelper(log.Default())) // Pass *testing.T here.
	iface.ReturnMockErrorAsResult = true                           // You can enable returning mock error as result of the method.

	// Create and set argument and return parameters of the method.
	iface.On_Method().Args(1).Rets(nil).Times(genmock.TimesAny)

	err := iface.Method(1) // Call the method.
	fmt.Println(err)

	err = iface.Mock.AssertExpectations() // Check that all method called.
	fmt.Println(err)

}

// Iface_Mock implements mock of the Iface interface.
type Iface_Mock struct {
	Mock *genmock.Mock
	t    genmock.TestingT

	ReturnMockErrorAsResult bool
}

// NewIface_Mock returns a new mock of Iface interface implementer.
// Takes *testing.T to stop failed testing process.
func NewIface_Mock(t genmock.TestingT) *Iface_Mock {
	return &Iface_Mock{
		Mock: genmock.NewMock(),
		t:    t,
	}
}

func (_m *Iface_Mock) Method(i int64) error {
	_ret, _err := _m.Mock.MethodCalled("Method", i)
	if _err != nil {
		if _m.ReturnMockErrorAsResult {
			return fmt.Errorf("(Method) call mock method: %w", _err)
		}
		_m.t.Fatalf("(Method) call mock method: %s", _err)
	}

	if len(_ret) != 1 {
		_err = fmt.Errorf("%w: want 1, got %d", genmock.ErrWrongReturnedLenght, len(_ret))
		if _m.ReturnMockErrorAsResult {
			return fmt.Errorf("(Method) check length of returned params: %w", _err)
		}
		_m.t.Fatalf("(Method) check length of returned params: %s", _err)
	}

	var _r0 error
	if _r := _ret[0]; _r != nil {
		if _v, _ok := _r.(error); _ok {
			_r0 = _v
		} else {
			_err = fmt.Errorf("%w [ret #0]: want 'error', got: %[2]T(%#[2]v)", genmock.ErrUnexpectedArgumentType, _r)
			if _m.ReturnMockErrorAsResult {
				return fmt.Errorf("(Method) check returned type: %w", _err)
			}
			_m.t.Fatalf("(Method) check returned type: %s", _err)
		}
	}

	return _r0
}

// Iface_MockSet_Method allows to set arguments and results of the mock call Method.
type Iface_MockSet_Method struct {
	Call *genmock.Call
}

// On_Method adds a call of the Method to mock.
func (_m *Iface_Mock) On_Method() Iface_MockSet_Method {
	call := genmock.NewCall(
		"Method",
		[]interface{}{genmock.AnythingOfType("int64")},
		[]interface{}{nil},
		1,
	)
	_m.Mock.AddCall(call)
	return Iface_MockSet_Method{Call: call}
}

// Args sets the exact values of the arguments.
func (_s Iface_MockSet_Method) Args(i int64) Iface_MockSet_Method {
	_s.Call.Args[0] = i
	return _s
}

// ArgsAnything sets the interface values of the arguments.
func (_s Iface_MockSet_Method) ArgsAnything(i interface{}) Iface_MockSet_Method {
	_s.Call.Args[0] = i
	return _s
}

// Arg_i_Matcher sets matcher of the i argument value.
func (_s Iface_MockSet_Method) Arg_i_Matcher(matcher func(i int64) bool) Iface_MockSet_Method {
	realMatcher := func(arg interface{}) bool {
		value, ok := arg.(int64)
		if !ok {
			return false
		}
		return matcher(value)
	}

	_s.Call.Args[0] = realMatcher
	return _s
}

// Rets sets the exact values of the result parameters.
func (_s Iface_MockSet_Method) Rets(_r0 error) Iface_MockSet_Method {
	_s.Call.Returns[0] = _r0
	return _s
}

// Times sets number of times to call this caller of the method.
func (_s Iface_MockSet_Method) Times(times int) Iface_MockSet_Method {
	_s.Call.Times = times
	return _s
}
Output:

<nil>
<nil>
Example (WithoutCodeGen)
package main

import (
	"fmt"
	"log"
	"time"

	"gitlab.com/so_literate/genmock"
)

// Data in the database.
type Data struct {
	ID   int
	Data string
}

// DB describes methods of the real object.
type DB interface {
	SetData(data *Data) error
	GetData(id int64) (*Data, error)
	Clean()
}

// DBMock your own type that implements DB interface.
type DBMock struct {
	mock *genmock.Mock
	t    genmock.TestingT // you can pass there a (t *testing.T).
}

func (db *DBMock) SetData(data *Data) error {
	db.t.Helper()
	ret, err := db.mock.MethodCalled("SetData", data)
	if err != nil {
		db.t.Fatalf("db.MethodCalled: %s", err)
	}

	res, err := genmock.ConvertArgError(0, ret)
	if err != nil {
		db.t.Fatalf("genmock.ConvertArgError (SetData): %s", err)
	}

	return res
}

func (db *DBMock) GetData(id int64) (*Data, error) {
	db.t.Helper()
	ret, err := db.mock.MethodCalled("GetData", id)
	if err != nil {
		db.t.Fatalf("db.MethodCalled: %s", err)
	}

	data, ok := ret[0].(*Data)
	if !ok {
		db.t.Fatalf("wrong type of the return argument: %#v", ret[0])
	}

	resErr, err := genmock.ConvertArgError(1, ret)
	if err != nil {
		db.t.Fatalf("genmock.ConvertArgError (GetData): %s", err)
	}

	return data, resErr
}

func (db *DBMock) Clean() {
	db.t.Helper()
	_, err := db.mock.MethodCalled("Clean")
	if err != nil {
		db.t.Fatalf("db.MethodCalled: %s", err)
	}
}

// dbCodeUser code that uses DB interface.
func dbCodeUser(db DB) (*Data, error) {
	err := db.SetData(&Data{Data: "some text"})
	if err != nil {
		return nil, fmt.Errorf("db.SetData: %w", err)
	}

	data, err := db.GetData(1)
	if err != nil {
		return nil, fmt.Errorf("db.GetData: %w", err)
	}

	db.Clean()

	return data, nil
}

func main() {
	mock := genmock.NewMock()

	matcher := func(arg interface{}) bool {
		data := arg.(*Data)
		return data.Data != ""
	}

	mock.
		AddCall(genmock.NewCall(
			"SetData",
			[]interface{}{matcher},    // First argument with custom compare.
			[]interface{}{error(nil)}, // Return nil as error.
			genmock.TimesAny,          // You can call it any times.
		)).
		AddCall(genmock.NewCall(
			"GetData",
			[]interface{}{genmock.AnythingOfType("int64")}, // First argument with soft compare by type only.
			[]interface{}{&Data{Data: "text"}, nil},        // Returns data and nil as error.
			1,                                              // You can call it only 1 time.
		))

	cleanCall := genmock.NewCall("Clean", nil, nil, 1)
	cleanCall.WaitTime = time.Millisecond * 10 // Wait 10 millisecond when mock found this call.

	mock.AddCall(cleanCall)

	db := &DBMock{
		mock: mock,
		t:    genmock.NewLoggerHelper(log.Default()), // pass *testing.T here.
	}

	data, err := dbCodeUser(db)
	if err != nil {
		fmt.Println(err)
		return
	}

	err = db.mock.AssertExpectations() // Check that all method was called.

	fmt.Println(data.Data)
	fmt.Println(err)

}
Output:

text
<nil>

Index

Examples

Constants

View Source
const Anything = AnythingArg("Anything")

Anything is a const to pass in expected argument value.

genmock.NewCall("MethodName", []interface{}{genmock.Anything}, nil, 1)
View Source
const TimesAny int = -1

TimesAny means that the method can be called any number of times.

Variables

View Source
var (
	// ErrMethodUnknown returns when method do not have registered calls.
	ErrMethodUnknown = errors.New("method unknown")
	// ErrLengthNotEqual returns in comparison of the arguments and the length of their different.
	ErrLengthNotEqual = errors.New("length is not equal")
	// ErrIsEqualNotTrue returns when custom matcher IsEqual returned false.
	ErrIsEqualNotTrue = errors.New("matcher IsEqual is not true")
	// ErrDeepEqualNotTrue returns when reflect.DeepEqual comparison retruned false.
	ErrDeepEqualNotTrue = errors.New("deep equal is not true")
	// ErrArgumentTypeUnknown returns when you used AnythingOfType as an argument
	// and reflect.TypeOf can't to got type of the argument.
	ErrArgumentTypeUnknown = errors.New("type of the argument unknown")
	// ErrUnexpectedArgumentType returns when you used AnythingOfType as an argument
	// and type of the actual argument is different.
	ErrUnexpectedArgumentType = errors.New("type of the argument is unexpected")
	// ErrExpectedCallNotFound returns when expected call of the method not found.
	// Maybe you used Anything or AnythingOfType or you just got wrong values of the actual arguments.
	ErrExpectedCallNotFound = errors.New("expected call not found")
	// ErrSimilarCallNotFound returns when mock can't to found even similar call of the method.
	// Mock checked all calls with Anything/AnythingOfType arguments but nothing.
	ErrSimilarCallNotFound = errors.New("similar call not found")
	// ErrRemainsSeveralTimesCallMethods returns when you mock has
	// mathods to call but nobody call them.
	ErrRemainsSeveralTimesCallMethods = errors.New("it remains several times to call method(s)")
	// ErrIndexOutOfRange returns in converting type methods to avoid panic.
	ErrIndexOutOfRange = errors.New("index out of range")
	// ErrWrongReturnedLenght returns when list of the result arguments have an unexpected length.
	ErrWrongReturnedLenght = errors.New("length of the returned params is wrong")
)

Functions

func ConvertArgBool

func ConvertArgBool(index int, args []interface{}) (bool, error)

ConvertArgBool converts interface argument by index to bool type.

func ConvertArgError

func ConvertArgError(index int, args []interface{}) (res, err error)

ConvertArgError converts interface argument by index to error type.

func ConvertArgInt

func ConvertArgInt(index int, args []interface{}) (int, error)

ConvertArgInt converts interface argument by index to int type.

func ConvertArgInt16

func ConvertArgInt16(index int, args []interface{}) (int16, error)

ConvertArgInt16 converts interface argument by index to int16 type.

func ConvertArgInt32

func ConvertArgInt32(index int, args []interface{}) (int32, error)

ConvertArgInt32 converts interface argument by index to int32 type.

func ConvertArgInt64

func ConvertArgInt64(index int, args []interface{}) (int64, error)

ConvertArgInt64 converts interface argument by index to int64 type.

func ConvertArgInt8

func ConvertArgInt8(index int, args []interface{}) (int8, error)

ConvertArgInt8 converts interface argument by index to int8 type.

func ConvertArgString

func ConvertArgString(index int, args []interface{}) (string, error)

ConvertArgString converts interface argument by index to string type.

func ConvertArgUint

func ConvertArgUint(index int, args []interface{}) (uint, error)

ConvertArgUint converts interface argument by index to uint type.

func ConvertArgUint16

func ConvertArgUint16(index int, args []interface{}) (uint16, error)

ConvertArgUint16 converts interface argument by index to uint16 type.

func ConvertArgUint32

func ConvertArgUint32(index int, args []interface{}) (uint32, error)

ConvertArgUint32 converts interface argument by index to uint32 type.

func ConvertArgUint64

func ConvertArgUint64(index int, args []interface{}) (uint64, error)

ConvertArgUint64 converts interface argument by index to uint64 type.

func ConvertArgUint8

func ConvertArgUint8(index int, args []interface{}) (uint8, error)

ConvertArgUint8 converts interface argument by index to uint8 type.

func IsArgumentListsEquals

func IsArgumentListsEquals(expArgs, actualArgs []interface{}) error

IsArgumentListsEquals compares two lists of the arguments. Arguments should be equal by type and value.

func IsArgumentListsSimilar

func IsArgumentListsSimilar(expArgs, actualArgs []interface{}) error

IsArgumentListsSimilar compare two lists of the arguments. Arguments may contains less accurate comparison types (Anything, AnythingOfType).

func IsArgumentsEquals

func IsArgumentsEquals(expArg, actualArg interface{}) error

IsArgumentsEquals checks arguments they are should be equal or expected argument should be matcher function.

func IsArgumentsSimilar

func IsArgumentsSimilar(expArg, actualArg interface{}) error

IsArgumentsSimilar compares arguments they are may be Anything, AnythingOfType or equal by type and value.

Types

type AnythingArg

type AnythingArg string

AnythingArg argument of the method may be any of type and value.

type AnythingOfTypeArg

type AnythingOfTypeArg string

AnythingOfTypeArg argument of the method may be any of value but with mane of type.

func AnythingOfType

func AnythingOfType(typeName string) AnythingOfTypeArg

AnythingOfType is a func to wrap type name to use as expected argument in method.

genmock.NewCall("MethodName", []interface{}{genmock.AnythingOfType("int")}, nil, 1)

type Call

type Call struct {
	// Name of the method.
	MethodName string
	// Arguments of the method to compare with called actual arguments.
	Args []interface{}
	// Arguments of the method to retun when mock found this method call.
	Returns []interface{}
	// The number of times to call this caller of the method.
	// -1 any times, 0 already called, 1 ... n times.
	Times int

	// Call will be block until it receives a message or is closed.
	WaitFor <-chan time.Time
	// Call will be sleep this time before return arguments.
	WaitTime time.Duration
	// Runs func RunFn(args) when mock found this method call.
	// It's useful when you want to unmarshall method arguments.
	RunFn func(args []interface{})
	// Calls panic(*PanicMessage) when mock found this method call.
	PanicMsg *string
}

Call way to call method, contains expected arguments, number of calls and other results of the call. You can set differents expected arguments per call or you can set univarsal set of the arguments.

func NewCall

func NewCall(methodName string, args, returns []interface{}, times int) *Call

NewCall takes basic call settings as a method name, expected arguments, returned arguments, and the number of possible retries. The other fields can be filled in manually.

type IsEqual

type IsEqual func(interface{}) bool

IsEqual it is type just for documentation. You can use this function to compare arguments according to your own rules.

matcher := func(arg interface{}) bool { return arg.(int) == 3 }
genmock.NewCall("MethodName", []interface{}{matcher}, nil, 1)

type Mock

type Mock struct {
	// contains filtered or unexported fields
}

Mock contains methods to add and call mocked interface methods.

func NewMock

func NewMock() *Mock

NewMock returns empty mock type. Feel free user default value of the Mock:

mock := genmock.NewMock()
mock := new(genmock.Mock)

func (*Mock) AddCall

func (m *Mock) AddCall(c *Call) *Mock

AddCall adds your expected call variant of method to list of methods.

mock := genmock.NewMock().AddCall(mockMethod1).AddCall(mockMethod2)

func (*Mock) AssertExpectations

func (m *Mock) AssertExpectations() error

AssertExpectations checks that all added methods was called or with TimesAny parameter. Returns an error with a full description of not called methods.

func (*Mock) MethodCalled

func (m *Mock) MethodCalled(methodName string, args ...interface{}) ([]interface{}, error)

MethodCalled tries to find method by name and actual arguments. First of all trying to find call with full equals arguments. If failed then trying to find similar call with Anything and AnythingOfType args. Returns the returned arguments and checks another call fields and calls them before returning the arguments.

// Method of the interface SetData(data sting) error
setData := genmock.NewCall("SetData", []interface{}{genmock.AnythingOfType("string")}, []interface{}{nil}, 1)
mock := genmock.NewMock().AddCall(setData)
ret, err := mock.MethodCalled("SetData", "some text data")

type TestingT

type TestingT interface {
	Fatalf(format string, args ...interface{})
	Helper()
}

TestingT interface for genereted mock type to pass *testing.T.

func NewLoggerHelper

func NewLoggerHelper(f fatalfer) TestingT

NewLoggerHelper returns TestingT implemetner to pass logger instead *testing.T.

Directories

Path Synopsis
_example
codegen/mock
Package mock contains mock implementer of the Database interface.
Package mock contains mock implementer of the Database interface.
cmd
genmock Module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL