agiledragon / gomonkey

gomonkey is a library to make monkey patching in unit tests easy
MIT License
1.93k stars 178 forks source link

Feature: A method to get the original value #103

Open Nomango opened 1 year ago

Nomango commented 1 year ago

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 commented 1 year ago

it's a good proposal

agiledragon commented 1 year ago

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

Nomango commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

// 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 commented 1 year ago

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

Nomango commented 1 year ago

比如

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 7 months ago

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 commented 7 months ago

Welcome to submit PR @Nomango

wu-cl commented 1 month ago

@agiledragon #166

agiledragon commented 3 weeks ago

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

agiledragon commented 3 weeks ago

@Nomango You can check #166

ytlw commented 1 week ago

@agiledragon #166

very good! thank you very much