Go語言單元測試模擬服務請求和接口返回
前言
這是Go單元測試從入門到放棄系列教程的第1篇,介紹了如何使用httptest和gock工具進行網絡測試。
在上一篇《Go單元測試從入門到放棄—0.單元測試基礎》中,我們介紹了Go語言編寫單元測試的基礎內容。
而實際工作中的業(yè)務場景往往會比較復雜,無論我們的代碼是作為server端對外提供服務或者還是我們依賴別人提供的網絡服務(調用別人提供的API接口)的場景,我們通常都不想在測試過程中真正的建立網絡連接。本文就專門介紹如何在上述兩種場景下mock網絡測試。
httptest
在Web開發(fā)場景下的單元測試,如果涉及到HTTP請求推薦大家使用Go標準庫 net/http/httptest 進行測試,能夠顯著提高測試效率。
在這一小節(jié),我們以常見的gin框架為例,演示如何為 http server 編寫單元測試。
假設我們的業(yè)務邏輯是搭建一個http server端,對外提供HTTP服務。我們編寫了一個helloHandler函數,用來處理用戶請求。
//?gin.go
package?httptest_demo
import?(
?"fmt"
?"net/http"
?"github.com/gin-gonic/gin"
)
//?Param?請求參數
type?Param?struct?{
?Name?string?`json:"name"`
}
//?helloHandler?/hello請求處理函數
func?helloHandler(c?*gin.Context)?{
?var?p?Param
?if?err?:=?c.ShouldBindJSON(&p);?err?!=?nil?{
??c.JSON(http.StatusOK,?gin.H{
???"msg":?"we?need?a?name",
??})
??return
?}
?c.JSON(http.StatusOK,?gin.H{
??"msg":?fmt.Sprintf("hello?%s",?p.Name),
?})
}
//?SetupRouter?路由
func?SetupRouter()?*gin.Engine?{
?router?:=?gin.Default()
?router.POST("/hello",?helloHandler)
?return?router
}
現在我們需要為helloHandler函數編寫單元測試,這種情況下我們就可以使用httptest這個工具mock一個HTTP請求和響應記錄器,讓我們的 server 端接收并處理我們 mock 的HTTP請求,同時使用響應記錄器來記錄 server 端返回的響應內容。
單元測試的示例代碼如下:
//?gin_test.go
package?httptest_demo
import?(
?"encoding/json"
?"net/http"
?"net/http/httptest"
?"strings"
?"testing"
?"github.com/stretchr/testify/assert"
)
func?Test_helloHandler(t?*testing.T)?{
?//?定義兩個測試用例
?tests?:=?[]struct?{
??name???string
??param??string
??expect?string
?}{
??{"base?case",?`{"name":?"liwenzhou"}`,?"hello?liwenzhou"},
??{"bad?case",?"",?"we?need?a?name"},
?}
?r?:=?SetupRouter()
?for?_,?tt?:=?range?tests?{
??t.Run(tt.name,?func(t?*testing.T)?{
???//?mock一個HTTP請求
???req?:=?httptest.NewRequest(
????"POST",??????????????????????//?請求方法
????"/hello",????????????????????//?請求URL
????strings.NewReader(tt.param),?//?請求參數
???)
???//?mock一個響應記錄器
???w?:=?httptest.NewRecorder()
???//?讓server端處理mock請求并記錄返回的響應內容
???r.ServeHTTP(w,?req)
???//?校驗狀態(tài)碼是否符合預期
???assert.Equal(t,?http.StatusOK,?w.Code)
???//?解析并檢驗響應內容是否復合預期
???var?resp?map[string]string
???err?:=?json.Unmarshal([]byte(w.Body.String()),?&resp)
???assert.Nil(t,?err)
???assert.Equal(t,?tt.expect,?resp["msg"])
??})
?}
}
執(zhí)行單元測試,查看測試結果
? go test -v
=== RUN Test_helloHandler
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /hello --> golang-unit-test-demo/httptest_demo.helloHandler (3 handlers)
=== RUN Test_helloHandler/base_case
[GIN] 2021/09/14 - 22:00:04 | 200 | 164.839µs | 192.0.2.1 | POST "/hello"
=== RUN Test_helloHandler/bad_case
[GIN] 2021/09/14 - 22:00:04 | 200 | 23.723µs | 192.0.2.1 | POST "/hello"
--- PASS: Test_helloHandler (0.00s)
--- PASS: Test_helloHandler/base_case (0.00s)
--- PASS: Test_helloHandler/bad_case (0.00s)
PASS
ok golang-unit-test-demo/httptest_demo 0.055s
通過這個示例我們就掌握了如何使用httptest在HTTP Server服務中為請求處理函數編寫單元測試了。
gock
上面的示例介紹了如何在HTTP Server服務類場景下為請求處理函數編寫單元測試,那么如果我們是在代碼中請求外部API的場景(比如通過API調用其他服務獲取返回值)又該怎么編寫單元測試呢?
例如,我們有以下業(yè)務邏輯代碼,依賴外部API:http://your-api.com/post提供的數據。
//?api.go
//?ReqParam?API請求參數
type?ReqParam?struct?{
?X?int?`json:"x"`
}
//?Result?API返回結果
type?Result?struct?{
?Value?int?`json:"value"`
}
func?GetResultByAPI(x,?y?int)?int?{
?p?:=?&ReqParam{X:?x}
?b,?_?:=?json.Marshal(p)
?//?調用其他服務的API
?resp,?err?:=?http.Post(
??"http://your-api.com/post",
??"application/json",
??bytes.NewBuffer(b),
?)
?if?err?!=?nil?{
??return?-1
?}
?body,?_?:=?ioutil.ReadAll(resp.Body)
?var?ret?Result
?if?err?:=?json.Unmarshal(body,?&ret);?err?!=?nil?{
??return?-1
?}
?//?這里是對API返回的數據做一些邏輯處理
?return?ret.Value?+?y
}
在對類似上述這類業(yè)務代碼編寫單元測試的時候,如果不想在測試過程中真正去發(fā)送請求或者依賴的外部接口還沒有開發(fā)完成時,我們可以在單元測試中對依賴的API進行mock。
這里推薦使用gock這個庫。
安裝
go?get?-u?gopkg.in/h2non/gock.v1
使用示例
使用gock對外部API進行mock,即mock指定參數返回約定好的響應內容。下面的代碼中mock了兩組數據,組成了兩個測試用例。
//?api_test.go
package?gock_demo
import?(
?"testing"
?"github.com/stretchr/testify/assert"
?"gopkg.in/h2non/gock.v1"
)
func?TestGetResultByAPI(t?*testing.T)?{
?defer?gock.Off()?//?測試執(zhí)行后刷新掛起的mock
?//?mock?請求外部api時傳參x=1返回100
?gock.New("http://your-api.com").
??Post("/post").
??MatchType("json").
??JSON(map[string]int{"x":?1}).
??Reply(200).
??JSON(map[string]int{"value":?100})
?//?調用我們的業(yè)務函數
?res?:=?GetResultByAPI(1,?1)
?//?校驗返回結果是否符合預期
?assert.Equal(t,?res,?101)
?//?mock?請求外部api時傳參x=2返回200
?gock.New("http://your-api.com").
??Post("/post").
??MatchType("json").
??JSON(map[string]int{"x":?2}).
??Reply(200).
??JSON(map[string]int{"value":?200})
?//?調用我們的業(yè)務函數
?res?=?GetResultByAPI(2,?2)
?//?校驗返回結果是否符合預期
?assert.Equal(t,?res,?202)
?assert.True(t,?gock.IsDone())?//?斷言mock被觸發(fā)
}
執(zhí)行上面寫好的單元測試,看一下測試結果。
? go test -v
=== RUN TestGetResultByAPI
--- PASS: TestGetResultByAPI (0.00s)
PASS
ok golang-unit-test-demo/gock_demo 0.054s
測試結果和預期的完全一致。
在這個示例中,為了讓大家能夠清晰的了解gock的使用,我特意沒有使用表格驅動測試。給大家留一個小作業(yè):自己動手把這個單元測試改寫成表格驅動測試的風格,就當做是對最近兩篇教程的復習和測驗。
?這里網管來當下課代表,大家可以把這個作業(yè)在公眾號私信發(fā)我,一起交流下答案。如果想摸魚也可以直接找我要答案,不過不給白嫖哦,必須來個三連:)。
總結
在日常工作開發(fā)中為代碼編寫單元測試時如何處理外部依賴是最常見的問題,本文介紹了如何使用httptest和gock工具mock相關依賴。
后面我們將更進一步,詳細介紹針對依賴MySQL和Redis的場景如何編寫單元測試,更多關于Go單元測試模擬服務請求和接口返回的資料請關注腳本之家其它相關文章!
相關文章
在 Golang 中實現 Cache::remember 方法詳解
這篇文章主要介紹了在 Golang 中實現 Cache::remember 方法詳解,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03

