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

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

