基於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 測試心得吧:
- 在測試之前,封裝好 get put等請求的方法,封裝到 httptestUtil,方便測試
- 靈活應用測試框架,比如
Testify
,能少寫很多 if 判斷,(主要用來判斷響應碼和響應實體)。剛開始我就是if else
寫了很多判斷,後來學了這個測試框架 - 測試程式碼儘量簡潔,保證可讀性和可維護性。否則寫一坨程式碼,容易邏輯混亂,而且看上去很煩,影響測試心智和測試準確性
- 如果遇到新的測試問題,儘量多搜多查多靜下來想一想,不要一股腦埋進去死挖問題原因。很可能你所糾結的問題並不是真正的原因
- 如果測試順利,那一切都好;如果測試不順利,期間搜了很多資料,花費了大量時間進行測試,那麼最後一定要寫部落格(或筆記),記錄所學所想所得,否則以後還會遇到類似的問題
- 測試程式碼最好貼到部落格(或筆記APP)上,方便以後檢視
- 最重要的一點,思路要清晰。測試很容易讓人頭大,煩躁,不要死磕,不妨停下來緩一緩,休息一下,讓大腦放鬆下來
參考資料:
1.Gin官方測試文件
2.基於golang gin框架的單元測試
3.用 Testify 來改善 GO 測試和模擬
本作品採用《CC 協議》,轉載必須註明作者和本文連結