在 Go 中實現(xiàn) Monkey Patch
背景
在進(jìn)行單元測試的時候,通過?testify框架?對測試函數(shù)的數(shù)據(jù)和所依賴的方法做 mock,但是單測出現(xiàn) panic。根據(jù)錯誤提示,被測試函數(shù)調(diào)用了 time.Now(), 因為會對比這個函數(shù)返回值, 所以本次單測沒有跑通過。下面介紹通過?monkey patch?來解決這個問題。
問題復(fù)現(xiàn)
示例代碼如下,HandleEvent()?處理一個 Webhook 的回調(diào)事件,使用 time.Now()?標(biāo)識事件處理的時間點:
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)
}
測試文件包含了設(shè)置測試功能、進(jìn)行初始化設(shè)置和模擬數(shù)據(jù)。EventSrv 接收 EventArgs 入?yún)ⅲ祷靥幚砗蟮?response,在沒有 mock 時間(CreatedAt)的情況下,執(zhí)行單測函數(shù)會報如下錯誤:

問題的原因是代碼在測試環(huán)境和主代碼中運行時,會有時延問題。這里的預(yù)期時間比實際時間大,因為我們在設(shè)置測試之前 mock 了時間(CreatedAt),而實際時間是在主代碼中創(chuàng)建的。
可以通過 Monkey Patch 的方式, 來解決類似在單元測試 Mock 數(shù)據(jù)狀態(tài)不一致問題。
Monkey Patch
Monkey Patch 是程序在本地擴(kuò)展、或修改程序?qū)傩缘囊环N方式。是指在運行時對類或模塊的動態(tài)修改,其目的是給現(xiàn)有的第三方代碼打上補丁,以解決沒有達(dá)到預(yù)期效果的問題或功能。一般用于動態(tài)語言,比如 Python 和 Ruby。有以下應(yīng)用場景:
在運行時替換掉 classes/methods/attributes/functions 修改/擴(kuò)展第三方 Lib 的行為,而不依賴源代碼 在運行時將 Patch 的結(jié)果應(yīng)用到內(nèi)存中的狀態(tài) 修復(fù)原來代碼存在的安全問題或行為修正
簡單來說就是 Monkey Patch 可以修改當(dāng)前運行的實例的變量狀態(tài)和行為。以上面說到的問題,就是修改 time.Now()來返回我們約定好的時間值。
雖然 Go 是靜態(tài)編譯語言,Mockey Patch 的作用域在 Runtime,但是通過 Go 的 unsafe 包,能夠?qū)?nèi)存中函數(shù)的地址替換為運行時函數(shù)的地址。具體的原理和實現(xiàn)方式參考 =>?Monkey Ptching in Go。
解決方案
Monkey?庫是 Monkey Patch 的一個 Go 版本實現(xiàn)。通過這個依賴包,修改 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 后,當(dāng)主代碼執(zhí)行到 time.Now()時,將指向到這個給定的函數(shù),返回自定義的 Mock 值。
注意:?因為 unsafe操作是不安全的,繞過了 Go 的內(nèi)存安全原則,所以應(yīng)該在測試環(huán)境中使用 Monkey Patch,并且只在需要的時候使用,確保真正需要 Mocking 的 testing 函數(shù)只使用這種方式。
小結(jié)
本文由一次單元測試沒有 mock 掉 time.Now()?的 case 引出 Monkey Patch ,介紹了它的特性和原理,并且通過 Monkey 的 Go 實現(xiàn), 解決我們在單測可能存在的一些 mock 數(shù)據(jù)不一致問題。
參考
Monkey Ptching in Go:https://bou.ke/blog/monkey-patching-in-go/ Monkey patch:https://en.wikipedia.org/wiki/Monkey_patch

