目錄一、httptest1.1 前置代碼準備1.2 介紹1.3 基本用法二、gock2.1介紹2.2 安裝2.3 基本使用2.4 舉個例子2.4.1 前置代碼2.4.2 測試用例 一、httptest 1.1 前置代碼準備 假設我們的業務邏輯是搭建一個http server端,對外提供HTTP服務。 ...
目錄
一、httptest
1.1 前置代碼準備
假設我們的業務邏輯是搭建一個http server端,對外提供HTTP服務。用來處理用戶登錄請求,用戶需要輸入郵箱,密碼。
package main
import (
regexp "github.com/dlclark/regexp2"
"github.com/gin-gonic/gin"
"net/http"
)
type UserHandler struct {
emailExp *regexp.Regexp
passwordExp *regexp.Regexp
}
func (u *UserHandler) RegisterRoutes(server *gin.Engine) {
ug := server.Group("/user")
ug.POST("/login", u.Login)
}
func NewUserHandler() *UserHandler {
const (
emailRegexPattern = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"
passwordRegexPattern = `^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$`
)
emailExp := regexp.MustCompile(emailRegexPattern, regexp.None)
passwordExp := regexp.MustCompile(passwordRegexPattern, regexp.None)
return &UserHandler{
emailExp: emailExp,
passwordExp: passwordExp,
}
}
type LoginRequest struct {
Email string `json:"email"`
Pwd string `json:"pwd"`
}
func (u *UserHandler) Login(ctx *gin.Context) {
var req LoginRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"msg": "參數不正確!"})
return
}
// 校驗郵箱和密碼是否為空
if req.Email == "" || req.Pwd == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"msg": "郵箱或密碼不能為空"})
return
}
// 正則校驗郵箱
ok, err := u.emailExp.MatchString(req.Email)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "系統錯誤!"})
return
}
if !ok {
ctx.JSON(http.StatusBadRequest, gin.H{"msg": "郵箱格式不正確"})
return
}
// 校驗密碼格式
ok, err = u.passwordExp.MatchString(req.Pwd)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "系統錯誤!"})
return
}
if !ok {
ctx.JSON(http.StatusBadRequest, gin.H{"msg": "密碼必須大於8位,包含數字、特殊字元"})
return
}
// 校驗郵箱和密碼是否匹配特定的值來確定登錄成功與否
if req.Email != "[email protected]" || req.Pwd != "hello#world123" {
ctx.JSON(http.StatusBadRequest, gin.H{"msg": "郵箱或密碼不匹配!"})
return
}
ctx.JSON(http.StatusOK, gin.H{"msg": "登錄成功!"})
}
func InitWebServer(userHandler *UserHandler) *gin.Engine {
server := gin.Default()
userHandler.RegisterRoutes(server)
return server
}
func main() {
uh := &UserHandler{}
server := InitWebServer(uh)
server.Run(":8080") // 在8080埠啟動伺服器
}
1.2 介紹
在 Web 開發場景下,單元測試經常需要模擬 HTTP 請求和響應。使用 httptest
可以讓我們在測試代碼中創建一個 HTTP 伺服器實例,並定義特定的請求和響應行為,從而模擬真實世界的網路交互,在Go語言中,一般都推薦使用Go標準庫 net/http/httptest
進行測試。
1.3 基本用法
使用 httptest
的基本步驟如下:
- 導入
net/http/httptest
包。 - 創建一個
httptest.Server
實例,並指定你想要的伺服器行為。 - 在測試代碼中使用
httptest.NewRequest
創建一個模擬的 HTTP 請求,並將其發送到httptest.Server
。 - 檢查響應內容或狀態碼是否符合預期。
以下是一個簡單的 httptest
用法示例
package main
import (
"bytes"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
func TestUserHandler_Login(t *testing.T) {
// 定義測試用例
testCases := []struct {
name string
reqBody string
wantCode int
wantBody string
}{
{
name: "登錄成功",
reqBody: `{"email": "[email protected]", "pwd": "hello#world123"}`,
wantCode: http.StatusOK,
wantBody: `{"msg": "登錄成功!"}`,
},
{
name: "參數不正確",
reqBody: `{"email": "[email protected]", "pwd": "hello#world123",}`,
wantCode: http.StatusBadRequest,
wantBody: `{"msg": "參數不正確!"}`,
},
{
name: "郵箱或密碼為空",
reqBody: `{"email": "", "pwd": ""}`,
wantCode: http.StatusBadRequest,
wantBody: `{"msg": "郵箱或密碼不能為空"}`,
},
{
name: "郵箱格式不正確",
reqBody: `{"email": "invalidemail", "pwd": "hello#world123"}`,
wantCode: http.StatusBadRequest,
wantBody: `{"msg": "郵箱格式不正確"}`,
},
{
name: "密碼格式不正確",
reqBody: `{"email": "[email protected]", "pwd": "invalidpassword"}`,
wantCode: http.StatusBadRequest,
wantBody: `{"msg": "密碼必須大於8位,包含數字、特殊字元"}`,
},
{
name: "郵箱或密碼不匹配",
reqBody: `{"email": "[email protected]", "pwd": "hello#world123"}`,
wantCode: http.StatusBadRequest,
wantBody: `{"msg": "郵箱或密碼不匹配!"}`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// 創建一個 gin 的上下文
server := gin.Default()
h := NewUserHandler()
h.RegisterRoutes(server)
// mock 創建一個 http 請求
req, err := http.NewRequest(
http.MethodPost, // 請求方法
"/user/login", // 請求路徑
bytes.NewBuffer([]byte(tc.reqBody)), // 請求體
)
// 斷言沒有錯誤
assert.NoError(t, err)
// 設置請求頭
req.Header.Set("Content-Type", "application/json")
// 創建一個響應
resp := httptest.NewRecorder()
// 服務端處理請求
server.ServeHTTP(resp, req)
// 斷言響應碼和響應體
assert.Equal(t, tc.wantCode, resp.Code)
// 斷言 JSON 字元串是否相等
assert.JSONEq(t, tc.wantBody, resp.Body.String())
})
}
}
在這個例子中,我們創建了一個簡單的 HTTP 請求,TestUserHandler_Login
函數定義了一個測試函數,用於測試用戶登錄功能的不同情況。
testCases
列表定義了多個測試用例,每個測試用例包含了測試名稱、請求體、期望的 HTTP 狀態碼和期望的響應體內容。- 使用
for
迴圈遍歷測試用例列表,每次迴圈創建一個新的測試子函數,併在其中模擬 HTTP 請求發送給登錄介面。 - 在每個測試子函數中,先創建一個 Gin 的預設上下文和用戶處理器
UserHandler
,然後註冊路由並創建一個模擬的 HTTP 請求。 - 通過
httptest.NewRecorder()
創建一個響應記錄器,使用server.ServeHTTP(resp, req)
處理模擬請求,得到響應結果。 - 最後使用斷言來驗證實際響應的 HTTP 狀態碼和響應體是否與測試用例中的期望一致。
最後,使用Goland 運行測試,結果如下:
二、gock
2.1介紹
gock 可以幫助你在測試過程中模擬 HTTP 請求和響應,這對於測試涉及外部 API 調用的應用程式非常有用。它可以讓你輕鬆地定義模擬請求,並驗證你的應用程式是否正確處理了這些請求。
GitHub 地址:github.com/h2non/gock
2.2 安裝
你可以通過以下方式安裝 gock:
go get -u github.com/h2non/gock
導入 gock 包:
import "github.com/h2non/gock"
2.3 基本使用
gock
的基本用法如下:
- 啟動攔截器:在測試開始前,使用
gock.New
函數啟動攔截器,並指定你想要攔截的功能變數名稱和埠。 - 定義攔截規則:你可以使用
gock.Intercept
方法來定義攔截規則,比如攔截特定的 URL、方法、頭部信息等。 - 設置響應:你可以使用
gock.NewJson
、gock.NewText
等方法來設置攔截後的響應內容。 - 運行測試:在定義了攔截規則和響應後,你可以運行測試,
gock
會攔截你的 HTTP 請求,並返回你設置的響應。
2.4 舉個例子
2.4.1 前置代碼
如果我們是在代碼中請求外部API的場景(比如通過API調用其他服務獲取返回值)又該怎麼編寫單元測試呢?
例如,我們有以下業務邏輯代碼,依賴外部API:http://your-api.com/post
提供的數據。
// 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
}
在對類似上述這類業務代碼編寫單元測試的時候,如果不想在測試過程中真正去發送請求或者依賴的外部介面還沒有開發完成時,我們可以在單元測試中對依賴的API進行mock。
2.4.2 測試用例
使用gock
對外部API進行mock,即mock指定參數返回約定好的響應內容。 下麵的代碼中mock了兩組數據,組成了兩個測試用例。
package gock_demo
import (
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/h2non/gock.v1"
)
func TestGetResultByAPI(t *testing.T) {
defer gock.Off() // 測試執行後刷新掛起的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})
// 調用我們的業務函數
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})
// 調用我們的業務函數
res = GetResultByAPI(2, 2)
// 校驗返回結果是否符合預期
assert.Equal(t, res, 202)
assert.True(t, gock.IsDone()) // 斷言mock被觸發
}
分享是一種快樂,開心是一種態度!