最近用Gin開發一個Web項目,做單元測試的時候遇到了一點問題。後面花了一天找到了解決方案,這在裡分享一下。
項目需求
- 目前排查問題不太友善,希望可以在http的resp中帶有logid,這樣可以幫助問題追溯。
- HTTP請求中如果帶有M-LOGID的話,優先使用該字段作為logid
方案
最直覺的做法是針對每個接口,在handler中增加擷取logid,更新logid,在傳回值中增加logid。
這樣做簡單直接,但是工作量比較大,總共29個接口,每個檔案handler方法均需要增加新的邏輯。簡單粗暴,但是可維護性差,後期修改任務量大。
經過大佬的提示,gin中的中間件可以實作批量處理的功能。引入中間件後,将更新logid,和往response裡面寫logid字段,寫入中間件。中間件可以在handler前,攔截下處理具體的請求,這樣就可以在不同的handler之前,統一添加處理邏輯,後面如果想要更改代碼,隻要在中間件内修改即可,可維護性更高。
問題
handler是一個沒有傳回值的函數裡,
func Handler(ctx *gin.Context) {...}
,這個函數通過
ctx.JSON()
将HTTP resp傳遞給調用方。以往的測試中,我們通過通路函數的
入參,或者
出參,判斷函數執行結果是否符合預期。但是cdn回調直接通過ctx.JSON()将resp通過架構,傳遞給了http調用方。我們無法在不侵入
func Handler(ctx *gin.Context) {...}
的前提下,擷取到resp.
那麼除了log和print,有木有一種方法可以通過比較
目标值和
實際值,直接得出
Handler(ctx *gin.Context){}
函數執行成功與否呢?
func Handler(ctx *gin.Context) {
...
defer func() {
ctx.JSON(http.StatusOK, resp)
}()
...
}
解決辦法
ctx.JSON執行過程
我們先了解一下ctx.JSON()的執行原理,ctx内部有個
ResponseWriter類型的interface,由這個接口通過Write() 将資料寫給Response body, 而後gin架構将resp傳遞給HTTP調用方。
type ResponseWriter interface {
http.ResponseWriter // 使用組合來實作繼承http.ResponseWriter
}
type ResponseWriter interface { // http裡面的ResponseWriter的定義
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
func (w *response) Write(data []byte) (n int, err error) {
...
w.written += int64(lenData) // ignoring errors, for errorKludge
w.w.Write(data)
...
return
}
ResponseWriter繼承于http.ResponseWriter,底層支撐
ResponseWriter
的是
response
結構體, 當我們引用
ResponseWriter
時,實際上引用的是
response
對象執行個體。write()方法擷取resp資料data,将其寫入http的resp body。
重寫writer()
看到這裡,我們發現了要往HTTP resp裡面寫的資料已經暴露在data裡面了。可否通過重寫write(),攔截resp裡面寫的資料呢?
我們構造一個帶有buffer的NewWriter,NewWriter重寫Write(), 這樣就可以将resp資料捕獲,我們将resp複寫一份,儲存到buffer裡面,這樣就可以在外部通路這個資料了。我們後面用這個writer,在handler接受ctx之前,替換掉ctx裡面的ResponseWriter,是不是就可以攔截和擷取http response裡面的内容呢?
type NewWriter struct {
gin.ResponseWriter
Buf []byte
}
func (w *NewWriter) Write(data []byte) (int, error) {
Buf = data
return w.ResponseWriter.Write(data) // http.response的write方法
}
測試
在測試handler.go的時候,提前把被測函數ctx的writer換成NewWriter,這樣就可以實作http resp的攔截驗證
func Handler(ctx *gin.Context) {
resp := ResponseInfo{}
defer func() {
ctx.JSON(http.StatusOK, resp)
}()
resp.Code, resp.Msg = parseStreamMeta(ctx)
}
替換
func GetNewWriter() *NewWriter {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
return &NewWriter{ResponseWriter: c.Writer} // logid = "", code = 0
}
type ResponseInfo struct {
Code int `json:"code"`
LogID string `json:"logid"`
}
newWriter := GetNewWriter() //
OverrideContextWriterAndSetLogID := OverrideContextWriterAndSetLogID(newWriter);
OverrideContextWriterAndSetLogID(tt.args.ctx)
Handler(tt.args.ctx) // 被測函數
resp := ResponseInfo{}
json.Unmarshal(newWriter.Buf, &resp)
if resp.Code != tt.code || resp.LogID != tt.logID {
t.Errorf("resp code=%d, wantRespCode=%d, resp logid=%s, exptect LogID=%s", resp.Code, tt.code, resp.LogID, tt.logID)
}
Tips
寫中間件改造ctx的writer時,因為傳入的ctx已經構造好,是以我們可以直接用ctx的Writer來構造NewWriter
func OverrideContextWriter() gin.HandlerFunc {
return func(ctx *gin.Context) {
NewWriter := &NewWriter{ResponseWriter: ctx.Writer} //
ctx.Writer = NewWriter
}
}
但是測試的時候,預設的ctx.Writer沒有實作,為nil. 而NewWriter的構造函數中,需要手動傳入一個已經實作好的ctx.Writer。如果不傳,執行的時候會報空指針。
經過查詢,用gin.CreateTestContext()可以幫助我們構造一個ctx執行個體,其中的ctx.Writer可以做為NewWriter構造函數的入參, 這樣可以在上下文沒有ctx執行個體的情況下,構造出ctx.Writer。
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
newWriter := &NewWriter{ResponseWriter: ctx.Writer}