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

Proposal: a new generic based API #17

Closed
xhd2015 opened this issue Mar 31, 2024 · 1 comment
Closed

Proposal: a new generic based API #17

xhd2015 opened this issue Mar 31, 2024 · 1 comment
Labels
proposal Proposal new ideas

Comments

@xhd2015
Copy link
Owner

xhd2015 commented Mar 31, 2024

Currently the mock.Mock(fn,interceptor) API provides a general way to setup mock.

When testing a logic with many external dependencies, the mock setup ends with a lot of results.GetFieldIndex(0).Set(...) call, as below:

func TestMyLargeFunc(t *testing.T){
    mock.Mock(pkg1.F, func(ctx context.Context, fn *core.FuncInfo, args, results core.Object) error {
		return nil
    })
    mock.Mock(pkg2.F, func(ctx context.Context, fn *core.FuncInfo, args, results core.Object) error {
                results.GetFieldIndex(0).Set(&Model{...})
		return nil
    })
    mock.Mock(pkg3.F, func(ctx context.Context, fn *core.FuncInfo, args, results core.Object) error {
                results.GetFieldIndex(0).Set(&Model{...})
		return nil
    })
    ...
    MyLargeFunc()
    ...
}

Thus, we propose a new API based on generic:

// T should be a function
func Patch[T](fn T, replacer T) func()

The above test can be write as following with better type safety:

func TestMyLargeFunc(t *testing.T){
    mock.Patch(pkg1.F, func() error {
		return nil
    })
    mock.Patch(pkg2.F, func(ctx context.Context) (*Model,error){
                return &Model{...},nil
    })
    mock.Patch(pkg3.F, func(ctx context.Context) (*Model,error){
                return &Model{...},nil
    })
    ...
    MyLargeFunc()
    ...
}

As for go1.17 and below, the function can just be:

// fn should be a function
func Patch(fn interface{}, replacer interface{}) func()

It should be compatible with the generic version.

As for the mock by name version, also add corresponding PatchByName and PatchMethodByName, but no generic param can be specified. And it is the user's responsibility to ensure the function signature matches.

NOTE: this Patch is not always feasible if some type in the function signature are not exported. In these case, just use the Mock API as before.

@xhd2015 xhd2015 added the proposal Proposal new ideas label Mar 31, 2024
@xhd2015
Copy link
Owner Author

xhd2015 commented Mar 31, 2024

Implementation:

package mock

import (
	"context"
	"fmt"
	"reflect"

	"github.com/xhd2015/xgo/runtime/core"
	"github.com/xhd2015/xgo/runtime/mock"
)

func Patch[T any](fn T, replacer T) func() {
	v := reflect.ValueOf(replacer)
	t := v.Type()
	if t.Kind() != reflect.Func {
		panic(fmt.Errorf("requires func, given %T", replacer))
	}
	if v.IsNil() {
		panic("replacer is nil")
	}

	nIn := t.NumIn()
	return mock.Mock(fn, func(ctx context.Context, fn *core.FuncInfo, args, results core.Object) error {
		// assemble arguments
		callArgs := make([]reflect.Value, nIn)
		src := 0
		dst := 0
		if fn.RecvType != "" {
			src++
		}
		if fn.FirstArgCtx {
			callArgs[0] = reflect.ValueOf(ctx)
			dst++
		}
		for i := 0; i < nIn-dst; i++ {
			callArgs[dst+i] = reflect.ValueOf(args.GetFieldIndex(src + i).Value())
		}

		// call the function
		var res []reflect.Value
		if !t.IsVariadic() {
			res = v.Call(callArgs)
		} else {
			res = v.CallSlice(callArgs)
		}

		// assign result
		nOut := len(res)
		for i := 0; i < nOut; i++ {
			results.GetFieldIndex(i).Set(res[i].Interface())
		}

		// check error
		if nOut > 0 {
			last := res[nOut-1].Interface()
			if last != nil {
				if err, ok := last.(error); ok {
					return err
				}
			}
		}

		return nil
	})
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal Proposal new ideas
Projects
None yet
Development

No branches or pull requests

1 participant