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

Feature: A method to get the original value #103

Closed
Nomango opened this issue Aug 10, 2022 · 18 comments
Closed

Feature: A method to get the original value #103

Nomango opened this issue Aug 10, 2022 · 18 comments

Comments

@Nomango
Copy link

Nomango commented Aug 10, 2022

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)()
})
@introspection3
Copy link

it's a good proposal

@agiledragon
Copy link
Owner

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

@Nomango
Copy link
Author

Nomango commented Sep 13, 2022

@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
Copy link
Author

Nomango commented Sep 13, 2022

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...)
	}

	// ...
}

@agiledragon
Copy link
Owner

// 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
})
....

@Nomango
Copy link
Author

Nomango commented Sep 15, 2022

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

@Nomango
Copy link
Author

Nomango commented Sep 15, 2022

比如

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 时,就没有很好的解决办法了

@nwanglu
Copy link

nwanglu commented Jan 12, 2023

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
}

@myzhan
Copy link
Contributor

myzhan commented Jun 28, 2023

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
Copy link
Contributor

myzhan commented Jun 28, 2023

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().

@Nomango
Copy link
Author

Nomango commented Nov 13, 2023

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()

@agiledragon
Copy link
Owner

Welcome to submit PR @Nomango

@wu-cl
Copy link
Contributor

wu-cl commented May 28, 2024

@agiledragon #166

@agiledragon
Copy link
Owner

@wu-cl It has been merged, 3ks!

@agiledragon
Copy link
Owner

@Nomango You can check #166

@ytlw
Copy link

ytlw commented Jun 27, 2024

@agiledragon #166

very good! thank you very much

@agiledragon
Copy link
Owner

@ytlw @wu-cl @myzhan https://www.jianshu.com/p/67f30e1e5a10?v=1720347782264

@Nomango
Copy link
Author

Nomango commented Aug 27, 2024

Thanks for all your work!

@Nomango Nomango closed this as completed Aug 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants