在 Go 中实现 Monkey Patch

SegmentFault

共 2789字,需浏览 6分钟

 · 2020-09-26

作者:lryong
来源:SegmentFault 思否社区 




背景


在进行单元测试的时候,通过 testify框架 对测试函数的数据和所依赖的方法做 mock,但是单测出现 panic。根据错误提示,被测试函数调用了 time.Now(), 因为会对比这个函数返回值, 所以本次单测没有跑通过。下面介绍通过 monkey patch 来解决这个问题。




问题复现


示例代码如下,HandleEvent() 处理一个 Webhook 的回调事件,使用 time.Now() 标识事件处理的时间点:


func (e *eventSrv) HandleEvent(ctx context.Context, args *EventArgs) (*Event, error) {
    event := &Event{
        CreatedAt: time.Now(),
        Messages:         args,
    }
      err := e.eventRepo.CreateEvents(&event)
    if err != nil {
        fmt.Println(`error occured while handing event:`, err)
        return nil, err
    }
    return event, nil
}


单元测试代码:


func TestService_HandleEvent_OK(t *testing.T) {
    var (
        ctx         = context.Background()
        createdTime = time.Now()
        args        = EventArgs{
            // Mock Data
            ...
        }
        createdTime = time.Now()
        event       = Event{
            Messages: args{
                CreatedAt: createdTime.String(),
            },
        }
    )

    eventMockRepo := &MockEventRepository{}
    eventMockRepo.On("HandleEvent", ctx, &args).
        Return(&event, nil)

    eventSrv := NewEventSrv(eventMockRepo)
    resp, err := eventSrv.HandleEvent(ctx, &args)

    assert.Nil(t, err)
    assert.Equal(t, resp, &event)
}


测试文件包含了设置测试功能、进行初始化设置和模拟数据。EventSrv 接收 EventArgs 入参,返回处理后的 response,在没有 mock 时间(CreatedAt)的情况下,执行单测函数会报如下错误:


问题的原因是代码在测试环境和主代码中运行时,会有时延问题。这里的预期时间比实际时间大,因为我们在设置测试之前 mock 了时间(CreatedAt),而实际时间是在主代码中创建的。


可以通过 Monkey Patch 的方式, 来解决类似在单元测试 Mock 数据状态不一致问题。




Monkey Patch


Monkey Patch 是程序在本地扩展、或修改程序属性的一种方式。是指在运行时对类或模块的动态修改,其目的是给现有的第三方代码打上补丁,以解决没有达到预期效果的问题或功能。一般用于动态语言,比如 Python 和 Ruby。有以下应用场景:


  1. 在运行时替换掉 classes/methods/attributes/functions
  2. 修改/扩展第三方 Lib 的行为,而不依赖源代码
  3. 在运行时将 Patch 的结果应用到内存中的状态
  4. 修复原来代码存在的安全问题或行为修正

简单来说就是 Monkey Patch 可以修改当前运行的实例的变量状态和行为。以上面说到的问题,就是修改 time.Now()来返回我们约定好的时间值。


虽然 Go 是静态编译语言,Mockey Patch 的作用域在 Runtime,但是通过 Go 的 unsafe 包,能够将内存中函数的地址替换为运行时函数的地址。具体的原理和实现方式参考 => Monkey Ptching in Go




解决方案


Monkey 库是 Monkey Patch 的一个 Go 版本实现。通过这个依赖包,修改 time.Now() 返回的时间:


func TestService_HandleEvent_OK(t *testing.T) {
    createdTime = time.Now()
  
      ...
  
      // resolve current time inconsistencies
    monkey.Patch(time.Now, func() time.Time {
        return createdTime
    })
  
      ...
  
}


Patch 后,当主代码执行到 time.Now()时,将指向到这个给定的函数,返回自定义的 Mock 值。


注意: 因为 unsafe操作是不安全的,绕过了 Go 的内存安全原则,所以应该在测试环境中使用 Monkey Patch,并且只在需要的时候使用,确保真正需要 Mocking 的 testing 函数只使用这种方式。




小结


本文由一次单元测试没有 mock 掉 time.Now() 的 case 引出 Monkey Patch ,介绍了它的特性和原理,并且通过 Monkey 的 Go 实现, 解决我们在单测可能存在的一些 mock 数据不一致问题。




参考

  1. Monkey Ptching in Go:https://bou.ke/blog/monkey-patching-in-go/
  2. Monkey patch:https://en.wikipedia.org/wiki/Monkey_patch



点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流。


- END -

浏览 4
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报