Golang實(shí)現(xiàn)單元測(cè)試中的接口層
上次我們已經(jīng)搞定了邏輯層的單元測(cè)試,這次我們來(lái)康康接口層的單元測(cè)試。接口層主要負(fù)責(zé)的就是請(qǐng)求的處理,最常見(jiàn)的就是 HTTP 請(qǐng)求的處理。
但針對(duì) 接口層 的單元測(cè)試其實(shí)是可以五花八門的。它并不像邏輯層和數(shù)據(jù)層一樣的通用,對(duì)于它的測(cè)試往往有很多路可以走。
由于使用的 HTTP 框架不同,單元測(cè)試的實(shí)現(xiàn)方式則不同。 既可以通過(guò)程序來(lái)模擬 HTTP 請(qǐng)求,也可以通過(guò)真實(shí)的 HTTP 請(qǐng)求來(lái)測(cè)試,通過(guò)借助外部的一些測(cè)試工具來(lái)實(shí)現(xiàn)。
所以本文只能給出一種思路,具體的實(shí)現(xiàn)方式還是要根據(jù)實(shí)際的框架來(lái)實(shí)現(xiàn)。
環(huán)境
本文以常用的 gin 框架為例,使用一種個(gè)人比較喜歡也非常簡(jiǎn)單的方式來(lái)實(shí)現(xiàn)單元測(cè)試。特點(diǎn)主要有:
- 不需要啟動(dòng)路由服務(wù)
- 復(fù)用已有的項(xiàng)目?jī)?nèi)的請(qǐng)求結(jié)構(gòu)
代碼
由于之前已經(jīng)貼過(guò),所以 service 層的 代碼這里就不贅述了
base case
package controller
import (
"context"
"github.com/gin-gonic/gin"
"go-demo/m/unit-test/entity"
)
//go:generate mockgen -source=./user.go -destination=../mock/user_service_mock.go -package=mock
type UserService interface {
AddUser(ctx context.Context, username string) (err error)
GetUser(ctx context.Context, userID int) (user *entity.User, err error)
}
type AddUserRequest struct {
Username string `json:"username" binding:"required"`
}
type GetUserRequest struct {
UserID int `form:"user_id" binding:"required"`
}
type GetUserResponse struct {
Username string `json:"username"`
}
type UserController struct {
UserService UserService
}
func NewUserController(userService UserService) *UserController {
return &UserController{UserService: userService}
}
func (uc *UserController) AddUser(ctx *gin.Context) {
req := &AddUserRequest{}
if err := ctx.BindJSON(req); err != nil {
return
}
if err := uc.UserService.AddUser(ctx, req.Username); err != nil {
ctx.JSON(400, gin.H{"error": err.Error()})
return
}
ctx.JSON(200, gin.H{"message": "success"})
}
func (uc *UserController) GetUser(ctx *gin.Context) {
req := &GetUserRequest{}
if err := ctx.BindQuery(req); err != nil {
return
}
user, err := uc.UserService.GetUser(ctx, req.UserID)
if err != nil {
ctx.JSON(400, gin.H{"error": err.Error()})
return
}
ctx.JSON(200, &GetUserResponse{Username: user.Username})
}- 既然之前我們 service 的單元測(cè)試已經(jīng)通過(guò),這次我們就需要 mock 的是 service 層的接口
mockgen -source=./user.go -destination=../mock/user_service_mock.go -package=mock - 這里我將請(qǐng)求和返回的結(jié)構(gòu) 如:GetUserRequest、GetUserResponse 放在了這里僅僅是為了方便展示代碼
單元測(cè)試
基礎(chǔ)代碼非常簡(jiǎn)單,就是我們常見(jiàn)的,最重要的讓我們來(lái)看看單元測(cè)試應(yīng)該怎么寫
工具方法
在編寫實(shí)際單元測(cè)試之前,我們需要一些工具方法來(lái)幫助我們構(gòu)建一些請(qǐng)求。
func createGetReqCtx(req interface{}, handlerFunc gin.HandlerFunc) (isSuccess bool, resp string) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
encode := structToURLValues(req).Encode()
c.Request, _ = http.NewRequest("GET", "/?"+encode, nil)
handlerFunc(c)
return w.Code == http.StatusOK, w.Body.String()
}
func createPostReqCtx(req interface{}, handlerFunc gin.HandlerFunc) (isSuccess bool, resp string) {
responseRecorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(responseRecorder)
body, _ := json.Marshal(req)
ctx.Request, _ = http.NewRequest("POST", "/", bytes.NewBuffer(body))
ctx.Request.Header.Set("Content-Type", "application/json")
handlerFunc(ctx)
return responseRecorder.Code == http.StatusOK, responseRecorder.Body.String()
}
// 將結(jié)構(gòu)體轉(zhuǎn)換為 URL 參數(shù)
func structToURLValues(s interface{}) url.Values {
v := reflect.ValueOf(s)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
t := v.Type()
values := url.Values{}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("form")
if tag == "" {
continue
}
value := v.Field(i).Interface()
values.Set(tag, valueToString(value))
}
return values
}
// 由于 get 請(qǐng)求常常參數(shù)并不會(huì)特別復(fù)雜,通常的幾種類型就應(yīng)該可以包括,有需要可以繼續(xù)添加
func valueToString(v interface{}) string {
switch v := v.(type) {
case int:
return strconv.Itoa(v)
case string:
return v
default:
return ""
}
}既然我們不想啟動(dòng)路由,其實(shí)最關(guān)鍵的問(wèn)題就在如何構(gòu)建一個(gè) gin.Context 來(lái)模擬正常的請(qǐng)求。
- 通過(guò)
gin.CreateTestContext創(chuàng)建一個(gè)我們需要模擬的 context - 通過(guò)
http.NewRequest來(lái)創(chuàng)建我們需要的請(qǐng)求結(jié)構(gòu)
單元測(cè)試
有了我們的工具方法,那么編寫單元測(cè)試的時(shí)候就非常方便了,mock 方法和之前類似,剩下要調(diào)用對(duì)應(yīng)的方法就可以了。并且這里可以復(fù)用我們已經(jīng)在原有程序中使用的 請(qǐng)求結(jié)構(gòu) 如 GetUserRequest 這樣就可以不需要重新勞動(dòng)了。
package controller
import (
"fmt"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"go-demo/m/unit-test/entity"
"go-demo/m/unit-test/mock"
)
func TestUserController_AddUser(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
req := &AddUserRequest{Username: "LinkinStar"}
mockUserService := mock.NewMockUserService(ctl)
mockUserService.EXPECT().AddUser(gomock.Any(), gomock.Any()).Return(nil)
userController := NewUserController(mockUserService)
success, resp := createPostReqCtx(req, userController.AddUser)
assert.True(t, success)
fmt.Println(resp)
}
func TestUserController_GetUser(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
req := &GetUserRequest{UserID: 1}
user := &entity.User{Username: "LinkinStar"}
mockUserService := mock.NewMockUserService(ctl)
mockUserService.EXPECT().GetUser(gomock.Any(), gomock.Any()).Return(user, nil)
userController := NewUserController(mockUserService)
success, resp := createGetReqCtx(req, userController.GetUser)
assert.True(t, success)
fmt.Println(resp)
}可以看到測(cè)試方法如出一轍,再詳細(xì)的話只需要對(duì)請(qǐng)求的返回值做解析然后進(jìn)行斷言即可。
問(wèn)題
當(dāng)然以上述方式來(lái)實(shí)現(xiàn)單元測(cè)試的話,是會(huì)遺漏一些問(wèn)題,畢竟偷懶是要有代價(jià)的。
- 路由路徑的問(wèn)題:可以看到上述的單元測(cè)試中并沒(méi)有注冊(cè)對(duì)應(yīng)的 url 地址,那么實(shí)際中可能會(huì)由于代碼路由的書寫錯(cuò)誤而導(dǎo)致 404 的情況
- 請(qǐng)求結(jié)構(gòu)字段錯(cuò)誤:由于我們復(fù)用了原有代碼中的請(qǐng)求結(jié)構(gòu),即使單詞拼寫錯(cuò)誤依然能成功,因?yàn)閮蛇叾家粯渝e(cuò),所以即使字段名稱與接口文檔不一致也無(wú)法發(fā)現(xiàn)。
針對(duì)這兩個(gè)問(wèn)題,我覺(jué)得可以由更加上層的測(cè)試來(lái)保證,由于這里僅僅是單元測(cè)試,我覺(jué)得這些代價(jià)還是可以接受的。并且,如果是使用 swagger 生成文檔的情況下,也能保證文檔和代碼的統(tǒng)一性。但在此還是要出來(lái)提個(gè)醒,畢竟實(shí)際問(wèn)題我還是遇到過(guò)的。
優(yōu)化點(diǎn)
當(dāng)然,這里的舉例還是過(guò)于簡(jiǎn)單,實(shí)際中的請(qǐng)求往往會(huì)比較復(fù)雜。
- 實(shí)際場(chǎng)景往往一些請(qǐng)求需要鑒權(quán),這個(gè)可以在根據(jù)實(shí)際你的鑒權(quán)方式在前面添加中間件統(tǒng)一來(lái)處理登錄就可以
- 其他類型的請(qǐng)求也是類似的如 PUT、DELETE 等
- 當(dāng)前只是簡(jiǎn)單的處理了正常的 200 HTTP Code 還會(huì)出現(xiàn)其他異常的情況也需要按實(shí)際接口進(jìn)行處理
總結(jié)
通常從現(xiàn)象來(lái)說(shuō),這一層的測(cè)試往往發(fā)現(xiàn)的問(wèn)題比較少,是由于這一層的邏輯少,測(cè)試下來(lái)最常見(jiàn)的問(wèn)題往往就是字段名稱和限制條件不滿足需求。所以其實(shí)從性價(jià)比的角度來(lái)說(shuō),單獨(dú)對(duì)這層拿出來(lái)測(cè)試往往比較低,故實(shí)際中見(jiàn)到的比較少。
不過(guò)話又說(shuō)回來(lái)了,本文的目的不僅僅是為了讓你了解到可以這樣寫單元測(cè)試,其中使用的方法往往還能再某些時(shí)候讓你復(fù)用 handler 的方法來(lái)保證系統(tǒng)的一致性。
到此這篇關(guān)于Golang實(shí)現(xiàn)單元測(cè)試中的接口層的文章就介紹到這了,更多相關(guān)Golang單元測(cè)試內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 深入理解Golang的單元測(cè)試和性能測(cè)試
- Golang單元測(cè)試與覆蓋率的實(shí)例講解
- golang 對(duì)私有函數(shù)進(jìn)行單元測(cè)試的實(shí)例
- GOLang單元測(cè)試用法詳解
- Golang使用ChatGPT生成單元測(cè)試實(shí)踐
- Golang?單元測(cè)試和基準(zhǔn)測(cè)試實(shí)例詳解
- Golang測(cè)試框架goconvey進(jìn)行單元測(cè)試流程介紹
- Golang單元測(cè)試中的技巧分享
- 詳解Golang中單元測(cè)試的使用
- Go語(yǔ)言:打造優(yōu)雅數(shù)據(jù)庫(kù)單元測(cè)試的實(shí)戰(zhàn)指南
相關(guān)文章
在Golang中正確的修改HTTPRequest的Host的操作方法
我們工作中經(jīng)常需要通過(guò)HTTP請(qǐng)求Server的服務(wù),比如腳本批量請(qǐng)求接口跑數(shù)據(jù),由于一些網(wǎng)關(guān)策略,部分Server會(huì)要求請(qǐng)求中Header里面附帶Host參數(shù),所以本文給大家介紹了如何在Golang中正確的修改HTTPRequest的Host,需要的朋友可以參考下2023-12-12
一百行Golang代碼實(shí)現(xiàn)簡(jiǎn)單并發(fā)聊天室
這篇文章主要為大家詳細(xì)介紹了一百行Golang代碼如何實(shí)現(xiàn)簡(jiǎn)單并發(fā)聊天室,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08
詳解Golang如何實(shí)現(xiàn)節(jié)假日不打擾用戶
這篇文章主要為大家介紹了Golang如何實(shí)現(xiàn)節(jié)假日不打擾用戶過(guò)程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
golang?Gin上傳文件返回前端及中間件實(shí)現(xiàn)示例
這篇文章主要為大家介紹了golang?Gin上傳文件返回前端及中間件實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04
Goland 關(guān)閉自動(dòng)移除未使用的包操作
這篇文章主要介紹了Goland 關(guān)閉自動(dòng)移除未使用的包操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12

