Go語言單元測試模擬服務(wù)請(qǐng)求和接口返回
前言
這是Go單元測試從入門到放棄系列教程的第1篇,介紹了如何使用httptest
和gock
工具進(jìn)行網(wǎng)絡(luò)測試。
在上一篇《Go單元測試從入門到放棄—0.單元測試基礎(chǔ)》中,我們介紹了Go語言編寫單元測試的基礎(chǔ)內(nèi)容。
而實(shí)際工作中的業(yè)務(wù)場景往往會(huì)比較復(fù)雜,無論我們的代碼是作為server
端對(duì)外提供服務(wù)或者還是我們依賴別人提供的網(wǎng)絡(luò)服務(wù)(調(diào)用別人提供的API接口)的場景,我們通常都不想在測試過程中真正的建立網(wǎng)絡(luò)連接。本文就專門介紹如何在上述兩種場景下mock
網(wǎng)絡(luò)測試。
httptest
在Web開發(fā)場景下的單元測試,如果涉及到HTTP請(qǐng)求推薦大家使用Go
標(biāo)準(zhǔn)庫 net/http/httptest
進(jìn)行測試,能夠顯著提高測試效率。
在這一小節(jié),我們以常見的gin
框架為例,演示如何為 http server 編寫單元測試。
假設(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ù)編寫單元測試,這種情況下我們就可以使用httptest
這個(gè)工具mock一個(gè)HTTP請(qǐng)求和響應(yīng)記錄器,讓我們的 server 端接收并處理我們 mock 的HTTP請(qǐng)求,同時(shí)使用響應(yīng)記錄器來記錄 server 端返回的響應(yīng)內(nèi)容。
單元測試的示例代碼如下:
//?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è)測試用例 ?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í)行單元測試,查看測試結(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ù)編寫單元測試了。
gock
上面的示例介紹了如何在HTTP Server服務(wù)類場景下為請(qǐng)求處理函數(shù)編寫單元測試,那么如果我們是在代碼中請(qǐng)求外部API的場景(比如通過API調(diào)用其他服務(wù)獲取返回值)又該怎么編寫單元測試呢?
例如,我們有以下業(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ù)代碼編寫單元測試的時(shí)候,如果不想在測試過程中真正去發(fā)送請(qǐng)求或者依賴的外部接口還沒有開發(fā)完成時(shí),我們可以在單元測試中對(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è)測試用例。
//?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?請(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í)行上面寫好的單元測試,看一下測試結(jié)果。
? go test -v
=== RUN TestGetResultByAPI
--- PASS: TestGetResultByAPI (0.00s)
PASS
ok golang-unit-test-demo/gock_demo 0.054s
測試結(jié)果和預(yù)期的完全一致。
在這個(gè)示例中,為了讓大家能夠清晰的了解gock
的使用,我特意沒有使用表格驅(qū)動(dòng)測試。給大家留一個(gè)小作業(yè):自己動(dòng)手把這個(gè)單元測試改寫成表格驅(qū)動(dòng)測試的風(fēng)格,就當(dāng)做是對(duì)最近兩篇教程的復(fù)習(xí)和測驗(yàn)。
?這里網(wǎng)管來當(dāng)下課代表,大家可以把這個(gè)作業(yè)在公眾號(hào)私信發(fā)我,一起交流下答案。如果想摸魚也可以直接找我要答案,不過不給白嫖哦,必須來個(gè)三連:)。
總結(jié)
在日常工作開發(fā)中為代碼編寫單元測試時(shí)如何處理外部依賴是最常見的問題,本文介紹了如何使用httptest
和gock
工具mock相關(guān)依賴。
后面我們將更進(jìn)一步,詳細(xì)介紹針對(duì)依賴MySQL和Redis的場景如何編寫單元測試,更多關(guān)于Go單元測試模擬服務(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)測文件修改
- 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 中實(shí)現(xiàn) Cache::remember 方法詳解
這篇文章主要介紹了在 Golang 中實(shí)現(xiàn) Cache::remember 方法詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03Golang實(shí)現(xiàn)優(yōu)雅的將struct轉(zhuǎn)換為map
在項(xiàng)目實(shí)踐中,有時(shí)候我們需要將struct結(jié)構(gòu)體轉(zhuǎn)為map映射表,然后基于map做數(shù)據(jù)裁剪或操作。那么下面我來介紹下常用的兩種轉(zhuǎn)換方式,希望對(duì)大家有所幫助2023-01-01Golang當(dāng)中的定時(shí)器實(shí)例詳解
這篇文章主要給大家介紹了關(guān)于Golang當(dāng)中定時(shí)器的相關(guān)資料,定時(shí)器的實(shí)現(xiàn)大家應(yīng)該都遇到過,最近在學(xué)習(xí)golang,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07