Go語言單元測(cè)試模擬服務(wù)請(qǐng)求和接口返回
前言
這是Go單元測(cè)試從入門到放棄系列教程的第1篇,介紹了如何使用httptest和gock工具進(jìn)行網(wǎng)絡(luò)測(cè)試。
在上一篇《Go單元測(cè)試從入門到放棄—0.單元測(cè)試基礎(chǔ)》中,我們介紹了Go語言編寫單元測(cè)試的基礎(chǔ)內(nèi)容。
而實(shí)際工作中的業(yè)務(wù)場景往往會(huì)比較復(fù)雜,無論我們的代碼是作為server端對(duì)外提供服務(wù)或者還是我們依賴別人提供的網(wǎng)絡(luò)服務(wù)(調(diào)用別人提供的API接口)的場景,我們通常都不想在測(cè)試過程中真正的建立網(wǎng)絡(luò)連接。本文就專門介紹如何在上述兩種場景下mock網(wǎng)絡(luò)測(cè)試。
httptest
在Web開發(fā)場景下的單元測(cè)試,如果涉及到HTTP請(qǐng)求推薦大家使用Go標(biāo)準(zhǔn)庫 net/http/httptest 進(jìn)行測(cè)試,能夠顯著提高測(cè)試效率。
在這一小節(jié),我們以常見的gin框架為例,演示如何為 http server 編寫單元測(cè)試。
假設(shè)我們的業(yè)務(wù)邏輯是搭建一個(gè)http server端,對(duì)外提供HTTP服務(wù)。我們編寫了一個(gè)helloHandler函數(shù),用來處理用戶請(qǐng)求。
//?gin.go
package?httptest_demo
import?(
?"fmt"
?"net/http"
?"github.com/gin-gonic/gin"
)
//?Param?請(qǐng)求參數(shù)
type?Param?struct?{
?Name?string?`json:"name"`
}
//?helloHandler?/hello請(qǐng)求處理函數(shù)
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
}
現(xiàn)在我們需要為helloHandler函數(shù)編寫單元測(cè)試,這種情況下我們就可以使用httptest這個(gè)工具mock一個(gè)HTTP請(qǐng)求和響應(yīng)記錄器,讓我們的 server 端接收并處理我們 mock 的HTTP請(qǐng)求,同時(shí)使用響應(yīng)記錄器來記錄 server 端返回的響應(yīng)內(nèi)容。
單元測(cè)試的示例代碼如下:
//?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)?{
?//?定義兩個(gè)測(cè)試用例
?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一個(gè)HTTP請(qǐng)求
???req?:=?httptest.NewRequest(
????"POST",??????????????????????//?請(qǐng)求方法
????"/hello",????????????????????//?請(qǐng)求URL
????strings.NewReader(tt.param),?//?請(qǐng)求參數(shù)
???)
???//?mock一個(gè)響應(yīng)記錄器
???w?:=?httptest.NewRecorder()
???//?讓server端處理mock請(qǐng)求并記錄返回的響應(yīng)內(nèi)容
???r.ServeHTTP(w,?req)
???//?校驗(yàn)狀態(tài)碼是否符合預(yù)期
???assert.Equal(t,?http.StatusOK,?w.Code)
???//?解析并檢驗(yàn)響應(yīng)內(nèi)容是否復(fù)合預(yù)期
???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í)行單元測(cè)試,查看測(cè)試結(jié)果
? 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
通過這個(gè)示例我們就掌握了如何使用httptest在HTTP Server服務(wù)中為請(qǐng)求處理函數(shù)編寫單元測(cè)試了。
gock
上面的示例介紹了如何在HTTP Server服務(wù)類場景下為請(qǐng)求處理函數(shù)編寫單元測(cè)試,那么如果我們是在代碼中請(qǐng)求外部API的場景(比如通過API調(diào)用其他服務(wù)獲取返回值)又該怎么編寫單元測(cè)試呢?
例如,我們有以下業(yè)務(wù)邏輯代碼,依賴外部API:http://your-api.com/post提供的數(shù)據(jù)。
//?api.go
//?ReqParam?API請(qǐng)求參數(shù)
type?ReqParam?struct?{
?X?int?`json:"x"`
}
//?Result?API返回結(jié)果
type?Result?struct?{
?Value?int?`json:"value"`
}
func?GetResultByAPI(x,?y?int)?int?{
?p?:=?&ReqParam{X:?x}
?b,?_?:=?json.Marshal(p)
?//?調(diào)用其他服務(wù)的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
?}
?//?這里是對(duì)API返回的數(shù)據(jù)做一些邏輯處理
?return?ret.Value?+?y
}
在對(duì)類似上述這類業(yè)務(wù)代碼編寫單元測(cè)試的時(shí)候,如果不想在測(cè)試過程中真正去發(fā)送請(qǐng)求或者依賴的外部接口還沒有開發(fā)完成時(shí),我們可以在單元測(cè)試中對(duì)依賴的API進(jìn)行mock。
這里推薦使用gock這個(gè)庫。
安裝
go?get?-u?gopkg.in/h2non/gock.v1
使用示例
使用gock對(duì)外部API進(jìn)行mock,即mock指定參數(shù)返回約定好的響應(yīng)內(nèi)容。下面的代碼中mock了兩組數(shù)據(jù),組成了兩個(gè)測(cè)試用例。
//?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()?//?測(cè)試執(zhí)行后刷新掛起的mock
?//?mock?請(qǐng)求外部api時(shí)傳參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})
?//?調(diào)用我們的業(yè)務(wù)函數(shù)
?res?:=?GetResultByAPI(1,?1)
?//?校驗(yàn)返回結(jié)果是否符合預(yù)期
?assert.Equal(t,?res,?101)
?//?mock?請(qǐng)求外部api時(shí)傳參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})
?//?調(diào)用我們的業(yè)務(wù)函數(shù)
?res?=?GetResultByAPI(2,?2)
?//?校驗(yàn)返回結(jié)果是否符合預(yù)期
?assert.Equal(t,?res,?202)
?assert.True(t,?gock.IsDone())?//?斷言mock被觸發(fā)
}
執(zhí)行上面寫好的單元測(cè)試,看一下測(cè)試結(jié)果。
? go test -v
=== RUN TestGetResultByAPI
--- PASS: TestGetResultByAPI (0.00s)
PASS
ok golang-unit-test-demo/gock_demo 0.054s
測(cè)試結(jié)果和預(yù)期的完全一致。
在這個(gè)示例中,為了讓大家能夠清晰的了解gock的使用,我特意沒有使用表格驅(qū)動(dòng)測(cè)試。給大家留一個(gè)小作業(yè):自己動(dòng)手把這個(gè)單元測(cè)試改寫成表格驅(qū)動(dòng)測(cè)試的風(fēng)格,就當(dāng)做是對(duì)最近兩篇教程的復(fù)習(xí)和測(cè)驗(yàn)。
?這里網(wǎng)管來當(dāng)下課代表,大家可以把這個(gè)作業(yè)在公眾號(hào)私信發(fā)我,一起交流下答案。如果想摸魚也可以直接找我要答案,不過不給白嫖哦,必須來個(gè)三連:)。
總結(jié)
在日常工作開發(fā)中為代碼編寫單元測(cè)試時(shí)如何處理外部依賴是最常見的問題,本文介紹了如何使用httptest和gock工具mock相關(guān)依賴。
后面我們將更進(jìn)一步,詳細(xì)介紹針對(duì)依賴MySQL和Redis的場景如何編寫單元測(cè)試,更多關(guān)于Go單元測(cè)試模擬服務(wù)請(qǐng)求和接口返回的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- goalng?結(jié)構(gòu)體?方法集?接口實(shí)例詳解
- go swagger生成接口文檔使用教程
- Go interface接口聲明實(shí)現(xiàn)及作用詳解
- Go?Ginrest實(shí)現(xiàn)一個(gè)RESTful接口
- Go語言fsnotify接口實(shí)現(xiàn)監(jiān)測(cè)文件修改
- GoFrame框架數(shù)據(jù)校驗(yàn)之校驗(yàn)結(jié)果Error接口對(duì)象
- Go調(diào)用Rust方法及外部函數(shù)接口前置
- 開發(fā)分布式醫(yī)療掛號(hào)系統(tǒng)MongoDB集成實(shí)現(xiàn)上傳醫(yī)院接口
- Go 請(qǐng)求兔子識(shí)別接口實(shí)現(xiàn)流程示例詳解
相關(guān)文章
Golang 讀取并解析SQL文件的實(shí)現(xiàn)方法
本文介紹了如何使用Go語言編寫一個(gè)簡單的函數(shù),用于讀取并解析SQL文件,通過一個(gè)函數(shù),我們可以輕松地將SQL文件中的語句提取出來,進(jìn)行后續(xù)的操作,感興趣的朋友跟隨小編一起看看吧2024-12-12
go語言實(shí)現(xiàn)簡易比特幣系統(tǒng)之交易簽名及校驗(yàn)功能
這篇文章主要介紹了go語言實(shí)現(xiàn)簡易比特幣系統(tǒng)之交易簽名及校驗(yàn)功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04
Golang項(xiàng)目搭配nginx部署反向代理負(fù)載均衡講解
這篇文章主要為大家介紹了Golang項(xiàng)目搭配nginx部署正反向代理負(fù)載均衡講解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04
Go?gRPC教程實(shí)現(xiàn)Simple?RPC
這篇文章主要為大家介紹了Go?gRPC教程實(shí)現(xiàn)Simple?RPC示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
Golang 實(shí)現(xiàn) RTP音視頻傳輸示例詳解
這篇文章主要為大家介紹了Golang實(shí)現(xiàn)RTP音視頻傳輸?shù)氖纠斀?,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
Golang實(shí)現(xiàn)gRPC的Proxy的原理解析
gRPC是Google開始的一個(gè)RPC服務(wù)框架, 是英文全名為Google Remote Procedure Call的簡稱,廣泛的應(yīng)用在有RPC場景的業(yè)務(wù)系統(tǒng)中,這篇文章主要介紹了Golang實(shí)現(xiàn)gRPC的Proxy的原理,需要的朋友可以參考下2021-09-09
golang字符串轉(zhuǎn)64位整數(shù)的示例代碼
這篇文章主要介紹了golang字符串轉(zhuǎn)64位整數(shù),在Go語言中,可以使用strconv包中的ParseInt函數(shù)將字符串轉(zhuǎn)換為64位整數(shù),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-09-09

