gomonkey icon indicating copy to clipboard operation
gomonkey copied to clipboard

Feature: A method to get the original value

Open Nomango opened this issue 2 years ago • 16 comments

Example

gomonkey.ApplyFunc(NewClient, func() *Client {
    // The original NewClient will be used here, but it has been actually replaced
    return NewClient()
})

Calling NewClient to create a instance before patch may solve this problem, but it is not convenient to do so in my scenario.

So I would like to use a GetOriginal method to do this, like:

var patches *gomonkey.Patches
patches = gomonkey.ApplyFunc(NewClient, func() *Client {
    original := patches.GetOriginal(NewClient)
    return original.(func() *Client)()
})

Nomango avatar Aug 10 '22 06:08 Nomango

it's a good proposal

introspection3 avatar Aug 26 '22 03:08 introspection3

What is your business secnario? I haven't get your problem yet.

agiledragon avatar Sep 12 '22 14:09 agiledragon

@agiledragon An example I find difficult to implement

// An interface with many methods
type Client interface {
	X()  int

	// ...
}

// A private client type that cannot ApplyMethod
type client struct {
	x, y int
}

func (c *client) X() int {
	return c.x
}

func NewClient(x, y int) Client {
	return &client{x: x, y: y}
}

gomonkey.ApplyFunc(NewClient, func(x, y int) Client {
	return NewClient(1, y) // want a fixed x, using original NewClient
})

Nomango avatar Sep 13 '22 08:09 Nomango

And my actual scenario if you need

// Expected usage for my utility package mockredis
patch := mockredis.Patch{
	Target: redis.NewClient,
	GenerateDouble: func(mockedCli *mockredis.Client) interface{} {
		return func(opts ...redis.Option) *redis.Client {
			opts = append(opts, redis.WithAddr(mockedCli.Addr)) // want a fixed addr, using original redis.NewClient
			return redis.NewClient(opts)
		}
	},
}
reset := mockredis.Init()
defer reset()

// package mockredis
var mockedCli *Client

func Init(patches ...Patch) context.CancelFunc {
	once.Do(func() {
		mockedCli = NewXXX()
	})

	monkeyPatches := gomonkey.NewPatches()
	for _, p := range patches {
		monkeyPatches.ApplyFunc(p.Target, p.GenerateDouble(mockedCli))
	}

	// ...
}

Version without original value

patch1 := mockredis.Patch{
	Target: redis.NewClient,
	CreateInstance: func(mockedCli *mockredis.Client) (returns []interface{}) {
		cli := redis.NewClient(redis.WithAddr(mockedCli.Addr)) // we lost option parameters
		return []interface{}{cli}
	},
}
patch2 := mockredis.Patch{
	Target: redis.NewFailoverClient, // Another func with same implementation. Although we won't use it in practice, the instance will still be created
	CreateInstance: func(mockedCli *mockredis.Client) (returns []interface{}) {
		cli := redis.NewClient(redis.WithAddr(mockedCli.Addr))
		return []interface{}{cli}
	},
}
reset := mockredis.Init(patch1, patch2)
defer reset()

// package mockredis
var mockedCli *Client

func Init(patches ...Patch) context.CancelFunc {
	once.Do(func() {
		mockedCli = NewXXX()
	})

	monkeyPatches := gomonkey.NewPatches()
	for _, p := range patches {
		returns := p.CreateInstance(mockedCli) // create instances, whatever need or not
		monkeyPatches.ApplyFuncReturn(p.Target, returns...)
	}

	// ...
}

Nomango avatar Sep 13 '22 08:09 Nomango

// An interface with many methods type Client interface { X() int

// ...

}

// A private client type that cannot ApplyMethod type client struct { x, y int }

func (c *client) X() int { return c.x }

func NewClient(x, y int) Client { return &client{x: x, y: y} }

//test code

type FakeClient struct { X, Y int }

func (c *FakeClient) X() int { return c.X }

c := &FakeClient{} patches := ApplyFunc(NewClient, func(x, y int) Client { return &FakeClient(X:1, Y: y) }) defer patches.Reset() patches.ApplyMethod(c, "X", func(_ *Client) int { return 2 }) ....

agiledragon avatar Sep 13 '22 15:09 agiledragon

@agiledragon 直接中文沟通吧,这个方法不行,因为FakeClient只是 mock 了一个X()方法,实际 client 是有很多参数和方法的,而且这个 fixed x 并不会生效,因为整个 Client 都是 fake 的,就算FakeClient里面放一个真的client进去,fixed x 也是没办法生效的

Nomango avatar Sep 15 '22 07:09 Nomango

比如

type client struct {
  x int
}

func (c *client) A() {
  // do something with c.x
}

func (c *client) B() {
  // do something with c.x
}

// and method C、D、E...

// 如何实现 FakeClient?

当 NewClient 返回的是个interface(没办法mock method),且有一个希望固定的入参 x 和一个透传的入参 y 时,就没有很好的解决办法了

Nomango avatar Sep 15 '22 07:09 Nomango

What is your business secnario? I haven't get your problem yet.

  1. is it possible to invoke original in double method?
  2. we're tring to wrap go method with additional steps plus origin implementation; thanks!
func (this *Patches) ApplyCore(target, double reflect.Value) *Patches {
	this.check(target, double)
	assTarget := *(*uintptr)(getPointer(target))
	original := replace(assTarget, uintptr(getPointer(double)))
	if _, ok := this.originals[assTarget]; !ok {
		this.originals[assTarget] = original
	}
	this.valueHolders[double] = double
	return this
}

nwanglu avatar Jan 12 '23 09:01 nwanglu

I think this feature is aslo very useful in unittests. For example, I need to make sure that a function call another function with correct arguments, but I want to keep calling the original callee.

myzhan avatar Jun 28 '23 09:06 myzhan

Here is a testcase to show what I need.

Convey("one func call origin", func() {
			var patches *Patches
			patches = ApplyFunc(fmt.Sprintf, func(format string, a ...interface{}) string {
				patches.Reset()
				So(format, ShouldEqual, "%s")
				return fmt.Sprintf(format, a...)
			})
			output := fmt.Sprintf("%s", "foobar")
			So(output, ShouldEqual, "foobar")
		})

I want to call the original fmt.Sprintf without calling patches.Reset().

myzhan avatar Jun 28 '23 11:06 myzhan

As a solution for this feature, I introduce a package similar to gomonkey: bytedance/mockey.

Quick example:

origin := Fun
mock := mockey.Mock(Fun).
    Origin(&origin).
    To(func(p string) string {
        return origin(p + "mocked")
    }).
    Build()
defer mock.UnPatch()

Nomango avatar Nov 13 '23 06:11 Nomango

Welcome to submit PR @Nomango

agiledragon avatar Nov 13 '23 14:11 agiledragon