Golang 學習——基於 Gin 框架進行 httptest 單元測試

相守之路發表於2020-05-18

基於Gin框架進行httptest單元測試

昨天晚上在學習慕課網的課程時,寫了個簡單的抽獎demo,打算簡單測試在併發場景下臨界資源是否被修改的問題。

然後前後折騰了好久才測試成功,記錄下自己在進行httptest單元測試時學到的知識。

以下程式碼是要的測試內容,大致有三個功能:

  • index 首頁,GET請求

  • 匯入抽獎使用者,POST請求

  • 抽獎,GET請求

    1.全域性變數及main函式

    記得初始化鎖,否則不起作用。

    // 使用者列表 共享變數(臨界資源)
    var userList []string
    // gin引擎
    var router *gin.Engine
    // 互斥鎖
    var mux sync.Mutex
    func main() {
     mux = sync.Mutex{} // 初始化鎖
     router.Run(":8080")
    }

    2.初始化路由

    主要初始化了三個功能的路由

    func init() {
     router = gin.Default()
     // 路由組
     userGroup := router.Group("/user")
     {
         // 首頁
         userGroup.GET("/index", Index)
         // 匯入使用者
         userGroup.POST("/import", ImportUsers)
         // 抽獎
         userGroup.GET("/lucky", GetLuckyUser)
     }
    }

    3.三個主要功能

    請求成功後,每個頁面都是返回一個字串(包含各自的資訊)

    3.1 首頁

    func Index(c *gin.Context) {
     c.String(http.StatusOK, "當前參與抽獎的使用者人數:%d", len(userList))
    }

    3.2 匯入使用者

    func ImportUsers(c *gin.Context) {
     strUsers := c.Query("users")
     users := strings.Split(strUsers, ",")
     // 在操作 全域性變數 userList 之前加互斥鎖,加完鎖記得釋放
     mux.Lock()
     defer mux.Unlock()
     // 統計當前已經在參加抽獎的使用者數量
     currUserCount := len(userList)
    
     // 將頁面提交的使用者匯入到 userList 中,參與抽獎
     for _, user := range users {
         user = strings.TrimSpace(user)
         if len(user) > 0 {
             userList = append(userList, user)
         }
     }
     // 統計當前總共參加抽獎人數
     userTotal := len(userList)
     c.String(http.StatusOK, "當前參與抽獎的使用者數量:%d,匯入的使用者數量:%d", userTotal, (userTotal - currUserCount))
    }

    3.3 抽獎

    func GetLuckyUser(c *gin.Context) {
     var user string
     // 在操作 全域性變數 userList 之前加互斥鎖,加完鎖記得釋放
     mux.Lock()
     defer mux.Unlock()
    
     count := len(userList)
     if count > 1 {
    
         seed := time.Now().UnixNano()
         // 以隨機數設定中獎使用者, [0,count)中的隨機值
         lottery_index := rand.New(rand.NewSource(seed)).Int31n(int32(count))
         user = userList[lottery_index]
         // 當前參與抽獎使用者減 1
         userList = append(userList[0:lottery_index], userList[lottery_index+1:]...)
         c.String(http.StatusOK, "中獎使用者為:%s,剩餘使用者數:%d", user, count-1)
    
     } else if count == 1 {
         user = userList[0]
         userList = userList[0:0] // 清空參與抽獎的使用者列表
         c.String(http.StatusOK, "中獎使用者為:%s,剩餘使用者數:%d", user, count-1)
     } else {
         c.String(http.StatusOK, "當前無參與抽獎的使用者,請匯入新的使用者。")
     }
    }

    httptestUtil.go檔案中主要封裝了以下工具函式:

2.1 ParseToStr 將map中的鍵值對輸出成querystring形式

// ParseToStr 將map中的鍵值對輸出成querystring形式
func ParseToStr(mp map[string]string) string {
    values := ""
    for key, val := range mp {
        values += "&" + key + "=" + val
    }
    temp := values[1:]
    values = "?" + temp
    return values
}

2.2 Get 根據特定請求uri,發起get請求返回響應

func Get(uri string, router *gin.Engine) *httptest.ResponseRecorder {
    // 構造get請求
    req := httptest.NewRequest("GET", uri, nil)
    // 初始化響應
    w := httptest.NewRecorder()

    // 呼叫相應的handler介面
    router.ServeHTTP(w, req)
    return w
}

2.3 ParseToStr 將map中的鍵值對輸出成querystring形式

構造POST請求,表單資料以 querystring 的形式加在uri之後

注意:form表單的引數可以通過 querystring 的形式附在URI地址後面進行傳遞

這種方式,POST 請求獲取引數是時要呼叫 c.Query("users"),而不是c.PostFprm("users"),更不是c.Param("users)

當然直接使用 c.ShouldBind() ,讓gin自動判斷是哪種方式的請求引數。
程式碼如下:

// PostForm 根據特定請求uri和引數param,以表單形式傳遞引數,發起post請求返回響應
func PostForm(uri string, param map[string]string, router *gin.Engine) *httptest.ResponseRecorder {
    req := httptest.NewRequest("POST", uri+ParseToStr(param), nil)
    // 初始化響應
    w := httptest.NewRecorder()
    // 呼叫相應handler介面
    router.ServeHTTP(w, req)
    return w
}

2.4 PostJson 根據特定請求uri和引數param,以Json形式傳遞引數,發起post請求返回響應

// PostJson 根據特定請求uri和引數param,以Json形式傳遞引數,發起post請求返回響應
func PostJson(uri string, param map[string]interface{}, router *gin.Engine) *httptest.ResponseRecorder {
    // 將引數轉化為json位元流
    jsonByte, _ := json.Marshal(param)
    // 構造post請求,json資料以請求body的形式傳遞
    req := httptest.NewRequest("POST", uri, bytes.NewReader(jsonByte))
    // 初始化響應
    w := httptest.NewRecorder()
    // 呼叫相應的handler介面
    router.ServeHTTP(w, req)
    return w
}

Golang規範是推薦一個方法寫一個測試函式,並且以Test開頭,後面跟方面名。

為了測試程式碼是否併發安全,就將三個功能的測試都寫在同一個測試函式裡,於是就命名為了TestMVC

func TestMVC(t *testing.T) {
    var w *httptest.ResponseRecorder
    assert := assert.New(t)

    // 1.測試 index 請求
    urlIndex := "/user/index"
    w = Get(urlIndex, router)
    assert.Equal(200, w.Code)
    assert.Equal("當前參與抽獎的使用者人數:0", w.Body.String())

    // 2.測試 import 請求,匯入使用者數
    var wg sync.WaitGroup // 定義wg, 用來阻塞 goroutine
    for i := 0; i < 100000; i++ {

        // 開一個等待
        wg.Add(1)
        go func(i int) { // i 不屬於臨界資源,是安全的
            defer wg.Done() // 一個 goroutine 跑完後要減1,

            // 測試 /user/import 請求,模擬從 form 表單中獲取資料
            param := make(map[string]string)
            param["users"] = "user" + strconv.Itoa(i)
            urlImport := "/user/import"
            w = PostForm(urlImport, param, router)
            assert.Equal(200, w.Code)

        }(i)
    }
    // 等待上面的協程執行完,再接著測試
    wg.Wait()
    // 3.測試 urlIndex 請求,檢視當前參與抽獎使用者是否為 for 迴圈總數
    w = Get(urlIndex, router)
    assert.Equal(200, w.Code)
    assert.Equal("當前參與抽獎的使用者人數:100000", w.Body.String())

    // 4.測試 抽獎
    urlLucky := "/user/lucky"
    w = Get(urlLucky, router)
    assert.Equal(200, w.Code)

    // 5.抽獎一次之後,再發起 index 請求,檢視檢視當前參與抽獎使用者是否減 1
    w = Get(urlIndex, router)
    assert.Equal(200, w.Code)
    assert.Equal("當前參與抽獎的使用者人數:99999", w.Body.String())
}

執行結果如圖:
抽獎功能_併發測試
如圖:
在我個人電腦上,測試執行耗時:9.21s;根據users欄位的名字也說明了執行了 100000次,因為是併發執行的,所以順序肯定不是從1到100000按序顯示的(誰搶到CPU資源誰執行)

從昨天晚上7點開始練習專案,進行單元測試,中間睡了6個小時吧。早上起來後,經過昨晚測試的磨練和學習,上午思路很清晰,不僅單元測試成功了,還將之前自己鼓搗的測試程式碼進行了重構和優化,直到今天上午11點多才正式完成。

第一次寫Golang的httptest單元測試,整個過程就是邊搜邊學邊實踐,最後總算成功了。寫一下 httptest 測試心得吧:

  1. 在測試之前,封裝好 get put等請求的方法,封裝到 httptestUtil,方便測試
  2. 靈活應用測試框架,比如Testify,能少寫很多 if 判斷,(主要用來判斷響應碼和響應實體)。剛開始我就是if else寫了很多判斷,後來學了這個測試框架
  3. 測試程式碼儘量簡潔,保證可讀性和可維護性。否則寫一坨程式碼,容易邏輯混亂,而且看上去很煩,影響測試心智和測試準確性
  4. 如果遇到新的測試問題,儘量多搜多查多靜下來想一想,不要一股腦埋進去死挖問題原因。很可能你所糾結的問題並不是真正的原因
  5. 如果測試順利,那一切都好;如果測試不順利,期間搜了很多資料,花費了大量時間進行測試,那麼最後一定要寫部落格(或筆記),記錄所學所想所得,否則以後還會遇到類似的問題
  6. 測試程式碼最好貼到部落格(或筆記APP)上,方便以後檢視
  7. 最重要的一點,思路要清晰。測試很容易讓人頭大,煩躁,不要死磕,不妨停下來緩一緩,休息一下,讓大腦放鬆下來

參考資料:
1.Gin官方測試文件
2.基於golang gin框架的單元測試
3.用 Testify 來改善 GO 測試和模擬

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章