天天看點

gin json 擷取_單元測試-Gin架構中沒有傳回值的函數

gin json 擷取_單元測試-Gin架構中沒有傳回值的函數

最近用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}