Golang實現(xiàn)單元測試中的接口層
上次我們已經(jīng)搞定了邏輯層的單元測試,這次我們來康康接口層的單元測試。接口層主要負責(zé)的就是請求的處理,最常見的就是 HTTP 請求的處理。
但針對 接口層 的單元測試其實是可以五花八門的。它并不像邏輯層和數(shù)據(jù)層一樣的通用,對于它的測試往往有很多路可以走。
由于使用的 HTTP 框架不同,單元測試的實現(xiàn)方式則不同。 既可以通過程序來模擬 HTTP 請求,也可以通過真實的 HTTP 請求來測試,通過借助外部的一些測試工具來實現(xiàn)。
所以本文只能給出一種思路,具體的實現(xiàn)方式還是要根據(jù)實際的框架來實現(xiàn)。
環(huán)境
本文以常用的 gin
框架為例,使用一種個人比較喜歡也非常簡單的方式來實現(xiàn)單元測試。特點主要有:
- 不需要啟動路由服務(wù)
- 復(fù)用已有的項目內(nèi)的請求結(jié)構(gòu)
代碼
由于之前已經(jīng)貼過,所以 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 的單元測試已經(jīng)通過,這次我們就需要 mock 的是 service 層的接口
mockgen -source=./user.go -destination=../mock/user_service_mock.go -package=mock
- 這里我將請求和返回的結(jié)構(gòu) 如:GetUserRequest、GetUserResponse 放在了這里僅僅是為了方便展示代碼
單元測試
基礎(chǔ)代碼非常簡單,就是我們常見的,最重要的讓我們來看看單元測試應(yīng)該怎么寫
工具方法
在編寫實際單元測試之前,我們需要一些工具方法來幫助我們構(gòu)建一些請求。
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 請求常常參數(shù)并不會特別復(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 "" } }
既然我們不想啟動路由,其實最關(guān)鍵的問題就在如何構(gòu)建一個 gin.Context 來模擬正常的請求。
- 通過
gin.CreateTestContext
創(chuàng)建一個我們需要模擬的 context - 通過
http.NewRequest
來創(chuàng)建我們需要的請求結(jié)構(gòu)
單元測試
有了我們的工具方法,那么編寫單元測試的時候就非常方便了,mock 方法和之前類似,剩下要調(diào)用對應(yīng)的方法就可以了。并且這里可以復(fù)用我們已經(jīng)在原有程序中使用的 請求結(jié)構(gòu) 如 GetUserRequest
這樣就可以不需要重新勞動了。
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) }
可以看到測試方法如出一轍,再詳細的話只需要對請求的返回值做解析然后進行斷言即可。
問題
當(dāng)然以上述方式來實現(xiàn)單元測試的話,是會遺漏一些問題,畢竟偷懶是要有代價的。
- 路由路徑的問題:可以看到上述的單元測試中并沒有注冊對應(yīng)的 url 地址,那么實際中可能會由于代碼路由的書寫錯誤而導(dǎo)致 404 的情況
- 請求結(jié)構(gòu)字段錯誤:由于我們復(fù)用了原有代碼中的請求結(jié)構(gòu),即使單詞拼寫錯誤依然能成功,因為兩邊都一樣錯,所以即使字段名稱與接口文檔不一致也無法發(fā)現(xiàn)。
針對這兩個問題,我覺得可以由更加上層的測試來保證,由于這里僅僅是單元測試,我覺得這些代價還是可以接受的。并且,如果是使用 swagger 生成文檔的情況下,也能保證文檔和代碼的統(tǒng)一性。但在此還是要出來提個醒,畢竟實際問題我還是遇到過的。
優(yōu)化點
當(dāng)然,這里的舉例還是過于簡單,實際中的請求往往會比較復(fù)雜。
- 實際場景往往一些請求需要鑒權(quán),這個可以在根據(jù)實際你的鑒權(quán)方式在前面添加中間件統(tǒng)一來處理登錄就可以
- 其他類型的請求也是類似的如 PUT、DELETE 等
- 當(dāng)前只是簡單的處理了正常的 200 HTTP Code 還會出現(xiàn)其他異常的情況也需要按實際接口進行處理
總結(jié)
通常從現(xiàn)象來說,這一層的測試往往發(fā)現(xiàn)的問題比較少,是由于這一層的邏輯少,測試下來最常見的問題往往就是字段名稱和限制條件不滿足需求。所以其實從性價比的角度來說,單獨對這層拿出來測試往往比較低,故實際中見到的比較少。
不過話又說回來了,本文的目的不僅僅是為了讓你了解到可以這樣寫單元測試,其中使用的方法往往還能再某些時候讓你復(fù)用 handler 的方法來保證系統(tǒng)的一致性。
到此這篇關(guān)于Golang實現(xiàn)單元測試中的接口層的文章就介紹到這了,更多相關(guān)Golang單元測試內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在Golang中正確的修改HTTPRequest的Host的操作方法
我們工作中經(jīng)常需要通過HTTP請求Server的服務(wù),比如腳本批量請求接口跑數(shù)據(jù),由于一些網(wǎng)關(guān)策略,部分Server會要求請求中Header里面附帶Host參數(shù),所以本文給大家介紹了如何在Golang中正確的修改HTTPRequest的Host,需要的朋友可以參考下2023-12-12一百行Golang代碼實現(xiàn)簡單并發(fā)聊天室
這篇文章主要為大家詳細介紹了一百行Golang代碼如何實現(xiàn)簡單并發(fā)聊天室,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-08-08詳解Golang如何實現(xiàn)節(jié)假日不打擾用戶
這篇文章主要為大家介紹了Golang如何實現(xiàn)節(jié)假日不打擾用戶過程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01golang?Gin上傳文件返回前端及中間件實現(xiàn)示例
這篇文章主要為大家介紹了golang?Gin上傳文件返回前端及中間件實現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪2022-04-04