在Go語言單元測(cè)試中解決HTTP網(wǎng)絡(luò)依賴問題
在開發(fā) Web 應(yīng)用程序時(shí),確保 HTTP 功能的正確性是至關(guān)重要的。然而,由于 Web 應(yīng)用程序通常涉及到與外部依賴的交互,編寫 HTTP 請(qǐng)求和響應(yīng)的有效測(cè)試變得具有挑戰(zhàn)性。在進(jìn)行單元測(cè)試時(shí),我們必須思考如何解決被測(cè)程序的外部依賴問題。
HTTP Server 測(cè)試
首先,我們來看下,站在 HTTP Server 端的角度,如何編寫應(yīng)用程序的測(cè)試代碼。
假設(shè)我們有一個(gè) HTTP Server 對(duì)外提供服務(wù),代碼如下:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"github.com/julienschmidt/httprouter"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
var users = []User{
{ID: 1, Name: "user1"},
}
func CreateUserHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
...
}
func GetUserHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
...
}
func setupRouter() *httprouter.Router {
router := httprouter.New()
router.POST("/users", CreateUserHandler)
router.GET("/users/:id", GetUserHandler)
return router
}
func main() {
router := setupRouter()
_ = http.ListenAndServe(":8000", router)
}這個(gè)服務(wù)監(jiān)聽 8000 端口,分別提供了兩個(gè) HTTP 接口:
POST /users 用來創(chuàng)建用戶。
GET /users/:id 用來獲取指定 ID 對(duì)應(yīng)的用戶信息。
為了保證業(yè)務(wù)的正確性,我們需要對(duì) CreateUserHandler 和 GetUserHandler 這兩個(gè) Handler 進(jìn)行單元測(cè)試。
我們先來看下用于創(chuàng)建用戶的 CreateUserHandler 函數(shù)是如何定義的:
func CreateUserHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
w.Header().Set("Content-Type", "application/json")
body, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error())
return
}
defer func() { _ = r.Body.Close() }()
u := User{}
if err := json.Unmarshal(body, &u); err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error())
return
}
u.ID = users[len(users)-1].ID + 1
users = append(users, u)
w.WriteHeader(http.StatusCreated)
}在這個(gè) Handler 中,首先寫入響應(yīng)頭 Content-Type: application/json,表示創(chuàng)建用戶的響應(yīng)內(nèi)容為 JSON 格式。
接著從請(qǐng)求體 r.Body 中讀取客戶端提交的用戶信息。
如果讀取請(qǐng)求體失敗,則寫入響應(yīng)狀態(tài)碼 400,表示客戶端提交的用戶信息有誤,并返回 JSON 錯(cuò)誤響應(yīng)。
接著,使用 json.Unmarshal 對(duì)請(qǐng)求體進(jìn)行 JSON 解碼,將數(shù)據(jù)填入 User 結(jié)構(gòu)體中。
如果 JSON 解碼失敗,則寫入響應(yīng)狀態(tài)碼 500,表示服務(wù)端出現(xiàn)了錯(cuò)誤,并返回 JSON 錯(cuò)誤響應(yīng)。
最終,將新創(chuàng)建的用戶信息保存到 users 切片中,并寫入響應(yīng)狀態(tài)碼 201,表示用戶創(chuàng)建成功。注意,根據(jù) RESTful 規(guī)范,這里并不需要返回響應(yīng)體。
下面,我們來分析下如何對(duì)這個(gè) Handler 函數(shù)編寫單元測(cè)試代碼。
首先,我們思考下 CreateUserHandler 這個(gè)函數(shù)都有哪些外部依賴?
從函數(shù)參數(shù)來看,我們需要一個(gè)用來表示 HTTP 響應(yīng)的 http.ResponseWriter,一個(gè)用來表示 HTTP 請(qǐng)求的 *http.Request,以及一個(gè)用來記錄 HTTP 請(qǐng)求路由參數(shù)的 httprouter.Params。
在函數(shù)內(nèi)部,則依賴了全局變量 users。
知道了這些外部依賴,那么,我們?nèi)绾尉帉憜卧獪y(cè)試才能解決這些外部依賴呢?
最直接的辦法,就是啟動(dòng)這個(gè) Web Server,然后在單元測(cè)試代碼中對(duì) POST /users 接口發(fā)送一個(gè) HTTP 請(qǐng)求,之后判斷程序的 HTTP 響應(yīng)結(jié)果以及 users 變量中的數(shù)據(jù),來驗(yàn)證 CreateUserHandler 函數(shù)的正確性。
但這種做法顯然超出了單元測(cè)試的范疇,更像是在做集成測(cè)試。單元測(cè)試的一個(gè)主要特征就是要隔離外部依賴,使用測(cè)試替身來替換依賴。
所以,我們應(yīng)該想辦法來制作測(cè)試替身。
我們先從最簡(jiǎn)單的 users 變量開始,想辦法在測(cè)試過程中替換掉 users。
users 僅是一個(gè)切片變量,用來保存用戶數(shù)據(jù),我們可以編寫一個(gè)函數(shù),將其內(nèi)容替換成測(cè)試數(shù)據(jù),代碼如下:
func setupTestUser() func() {
defaultUsers := users
users = []User{
{ID: 1, Name: "test-user1"},
}
return func() {
users = defaultUsers
}
}setupTestUser 函數(shù)內(nèi)部為全局變量 users 進(jìn)行了重新賦值,并返回一個(gè)匿名函數(shù),這個(gè)匿名函數(shù)可以將 users 變量值恢復(fù)。
在測(cè)試期間可以這樣使用:
func TestCreateUserHandler(t *testing.T) {
cleanup := setupTestUser()
defer cleanup()
...
}在測(cè)試最開始時(shí)調(diào)用 setupTestUser 來初始化測(cè)試數(shù)據(jù),使用 defer 語句實(shí)現(xiàn)測(cè)試函數(shù)退出時(shí)恢復(fù) users 數(shù)據(jù)。
接下來,我們需要構(gòu)造一個(gè)表示 HTTP 響應(yīng)的 http.ResponseWriter。
幸運(yùn)的是,這并不需要費(fèi)多少力氣,Go 語言官方早就想到了這個(gè)訴求,為我們提供了 net/http/httptest 標(biāo)準(zhǔn)庫,這個(gè)庫實(shí)現(xiàn)了一些專門用來進(jìn)行網(wǎng)絡(luò)測(cè)試的實(shí)用工具。
構(gòu)造一個(gè)測(cè)試用的 HTTP 響應(yīng)對(duì)象僅需一行代碼就能完成:
w := httptest.NewRecorder()
得到的 w 變量實(shí)現(xiàn)了 http.ResponseWriter 接口,可以直接傳遞給 Handler 函數(shù)。
要想構(gòu)造一個(gè)表示 HTTP 請(qǐng)求的 *http.Request 對(duì)象,同樣非常簡(jiǎn)單:
body := strings.NewReader(`{"name": "user2"}`)
req := httptest.NewRequest("POST", "/users", body)使用 httptest.NewRequest 創(chuàng)建的 req 變量正是 *http.Request 類型,它包含了請(qǐng)求方法、路徑、請(qǐng)求體。
現(xiàn)在,我們只差一個(gè)用來記錄 HTTP 請(qǐng)求路由參數(shù)的 httprouter.Params 類型對(duì)象沒有構(gòu)造了。
httprouter.Params 是由 httprouter 這個(gè)第三方包提供的,httprouter 是一個(gè)高性能的 HTTP 路由,兼容 net/http 標(biāo)準(zhǔn)庫。
它提供了 (*httprouter.Router).ServeHTTP 方法,可以調(diào)用請(qǐng)求對(duì)應(yīng)的 Handler 函數(shù)。即可以根據(jù)請(qǐng)求對(duì)象 *http.Request,自動(dòng)調(diào)用 CreateUserHandler 函數(shù)。
在調(diào)用 Handler 函數(shù)時(shí),httprouter 會(huì)解析請(qǐng)求中的路由參數(shù)保存在 httprouter.Params 對(duì)象中并傳給 Handler,所以這個(gè)對(duì)象無需我們手動(dòng)構(gòu)造。
現(xiàn)在,單元測(cè)試函數(shù)的邏輯就清晰了:
func TestCreateUserHandler(t *testing.T) {
cleanup := setupTestUser()
defer cleanup()
w := httptest.NewRecorder()
body := strings.NewReader(`{"name": "user2"}`)
req := httptest.NewRequest("POST", "/users", body)
router := setupRouter()
router.ServeHTTP(w, req)
}根據(jù)前文的講解,我們構(gòu)造了單元測(cè)試所需的依賴項(xiàng)。
setupRouter() 返回 *httprouter.Router 對(duì)象,當(dāng)代碼執(zhí)行到 router.ServeHTTP(w, req) 時(shí),就會(huì)根據(jù)傳遞的 req 參數(shù),自動(dòng)調(diào)用與之匹配的 Handler,即被測(cè)試函數(shù) CreateUserHandler。
接下來,我們要做的就是判斷 CreateUserHandler 函數(shù)執(zhí)行后的結(jié)果是否正確。
完整單元測(cè)試代碼如下:
package main
import (
"encoding/json"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCreateUserHandler(t *testing.T) {
cleanup := setupTestUser()
defer cleanup()
w := httptest.NewRecorder()
body := strings.NewReader(`{"name": "user2"}`)
req := httptest.NewRequest("POST", "/users", body)
router := setupRouter()
router.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
assert.Equal(t, "", w.Body.String())
assert.Equal(t, 2, len(users))
u2, _ := json.Marshal(users[1])
assert.Equal(t, `{"id":2,"name":"user2"}`, string(u2))
}這里引入了第三方包 testify 用來進(jìn)行斷言操作,assert.Equal 能夠判斷兩個(gè)對(duì)象是否相等,這可以簡(jiǎn)化代碼,不再需要使用 if 來判斷了。更多關(guān)于 testify 包的使用,可以查看官方文檔。
我們首先斷言了響應(yīng)狀態(tài)碼是否為 201。
接著又?jǐn)嘌粤隧憫?yīng)頭的 Content-Type 字段是否為 application/json。
然后判斷了響應(yīng)內(nèi)容是否為空。
最后,通過 users 中的值來判斷用戶信息是否保存正確。
使用 go test 來執(zhí)行測(cè)試函數(shù):
$ go test -v -run="TestCreateUserHandler" . === RUN TestCreateUserHandler --- PASS: TestCreateUserHandler (0.00s) PASS ok github.com/jianghushinian/blog-go-example/test/http/server 0.544s
測(cè)試通過。
至此,我們成功為 CreateUserHandler 函數(shù)編寫了一個(gè)單元測(cè)試。
不過,這個(gè)單元測(cè)試僅覆蓋了正常邏輯,CreateUserHandler 方法返回 400 和 500 兩種狀態(tài)碼的邏輯沒有被測(cè)試覆蓋,這兩種場(chǎng)景就留做作業(yè)你自己來完成吧。
接下來,我們?cè)贋楂@取用戶信息的函數(shù) GetUserHandler 編寫一個(gè)單元測(cè)試。
先來看下 GetUserHandler 函數(shù)的定義:
func GetUserHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
userID, _ := strconv.Atoi(ps[0].Value)
w.Header().Set("Content-Type", "application/json")
for _, u := range users {
if u.ID == userID {
user, _ := json.Marshal(u)
_, _ = w.Write(user)
return
}
}
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"msg":"notfound"}`))
}獲取用戶信息的邏輯,相對(duì)簡(jiǎn)單一點(diǎn)。
首先從 HTTP 請(qǐng)求的路徑參數(shù)中獲取用戶 ID。
然后判斷這個(gè) ID 對(duì)應(yīng)的用戶信息是否存在,如果存在就返回用戶信息。
不存在,則寫入 404 狀態(tài)碼,并返回 notfound 信息。
有了前文對(duì) CreateUserHandler 函數(shù)編寫測(cè)試的經(jīng)驗(yàn),想必如何對(duì) GetUserHandler 函數(shù)進(jìn)行測(cè)試你已經(jīng)輕車熟路了。
以下是我為其編寫的測(cè)試代碼:
func TestGetUserHandler(t *testing.T) {
cleanup := setupTestUser()
defer cleanup()
type want struct {
code int
body string
}
tests := []struct {
name string
args int
want want
}{
{
name: "get test-user1",
args: 1,
want: want{
code: 200,
body: `{"id":1,"name":"test-user1"}`,
},
},
{
name: "get user not found",
args: 2,
want: want{
code: 404,
body: `{"msg":"notfound"}`,
},
},
}
router := setupRouter()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", fmt.Sprintf("/users/%d", tt.args), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, tt.want.code, w.Code)
assert.Equal(t, tt.want.body, w.Body.String())
})
}
}獲取用戶信息的單元測(cè)試代碼,在測(cè)試執(zhí)行開始,同樣使用 setupTestUser 函數(shù)來初始化測(cè)試數(shù)據(jù),并使用 defer 來完成數(shù)據(jù)恢復(fù)。
這次為了提高測(cè)試覆蓋率,我對(duì) GetUserHandler 函數(shù)的正常響應(yīng)以及返回 404 狀態(tài)碼的異常響應(yīng)場(chǎng)景都進(jìn)行了測(cè)試。
這里使用了表格測(cè)試,
除了使用表格測(cè)試的形式,其他測(cè)試邏輯與 CreateUserHandler 的單元測(cè)試邏輯基本相同,我就不過多介紹了。
使用 go test 來執(zhí)行測(cè)試函數(shù):
$ go test -v -run="TestGetUserHandler" .
=== RUN TestGetUserHandler
=== RUN TestGetUserHandler/get_test-user1
=== RUN TestGetUserHandler/get_user_not_found
--- PASS: TestGetUserHandler (0.00s)
--- PASS: TestGetUserHandler/get_test-user1 (0.00s)
--- PASS: TestGetUserHandler/get_user_not_found (0.00s)
PASS
ok github.com/jianghushinian/blog-go-example/test/http/server 0.516s表格測(cè)試的兩個(gè)用例都通過了測(cè)試。
HTTP Client 測(cè)試
接下來,我們來看下,站在 HTTP Client 端的角度,如何編寫應(yīng)用程序的測(cè)試代碼。
假設(shè)我們有一個(gè)進(jìn)程監(jiān)控程序,能夠檢測(cè)某個(gè)進(jìn)程是否正在執(zhí)行,如果進(jìn)程退出,就發(fā)送一條消息通知到飛書群。
代碼如下:
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strconv"
"syscall"
"time"
)
func monitor(pid int) (*Result, error) {
for {
// 檢查進(jìn)程是否存在
err := syscall.Kill(pid, 0)
if err != nil {
log.Printf("Process %d exited\n", pid)
webhook := os.Getenv("WEBHOOK")
return sendFeishu(fmt.Sprintf("Process %d exited", pid), webhook)
}
log.Printf("Process %d is running\n", pid)
time.Sleep(1 * time.Second)
}
}
func main() {
if len(os.Args) != 2 {
log.Println("Usage: ./monitor <pid>")
return
}
pid, err := strconv.Atoi(os.Args[1])
if err != nil {
log.Printf("Invalid pid: %s\n", os.Args[1])
return
}
result, err := monitor(pid)
if err != nil {
log.Fatal(err)
}
log.Println(result)
}這個(gè)程序可以通過 ./monitor <pid> 形式啟動(dòng)。
monitor 函數(shù)內(nèi)部有一個(gè)循環(huán),會(huì)根據(jù)傳遞進(jìn)來的進(jìn)程 PID 不斷的來檢測(cè)對(duì)應(yīng)進(jìn)程是否存在。
如果不存在,則說明進(jìn)程已經(jīng)停止,然后調(diào)用 sendFeishu 函數(shù)發(fā)送消息通知到指定的飛書 webhook 地址。
monitor 函數(shù)會(huì)將 sendFeishu 函數(shù)的返回結(jié)果原樣返回。
sendFeishu 函數(shù)實(shí)現(xiàn)如下:
type Message struct {
Content struct {
Text string `json:"text"`
} `json:"content"`
MsgType string `json:"msg_type"`
}
type Result struct {
StatusCode int `json:"StatusCode"`
StatusMessage string `json:"StatusMessage"`
Code int `json:"code"`
Data any `json:"data"`
Msg string `json:"msg"`
}
func sendFeishu(content, webhook string) (*Result, error) {
msg := Message{
Content: struct {
Text string `json:"text"`
}{
Text: content,
},
MsgType: "text",
}
body, _ := json.Marshal(msg)
resp, err := http.Post(webhook, "application/json", bytes.NewReader(body))
if err != nil {
return nil, err
}
defer func() { _ = resp.Body.Close() }()
result := new(Result)
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
return nil, err
}
if result.Code != 0 {
return nil, fmt.Errorf("code: %d, error: %s", result.Code, result.Msg)
}
return result, nil
}sendFeishu 函數(shù)能夠?qū)鬟f進(jìn)來的消息發(fā)送到指定的 webhook 地址。
至于內(nèi)部具體邏輯,我們并不需要關(guān)心,只當(dāng)作第三方包來使用即可,僅需要知道它最終會(huì)返回 *Result 對(duì)象。
現(xiàn)在我們需要對(duì) monitor 函數(shù)進(jìn)行測(cè)試。
我們同樣需要先分析下 monitor 函數(shù)的外部依賴是什么。
首先 monitor 函數(shù)的參數(shù) pid 是一個(gè) int 類型,不難構(gòu)造。
monitor 函數(shù)內(nèi)部調(diào)用了 sendFeishu 函數(shù),并且將 sendFeishu 的返回結(jié)果原樣返回,所以 sendFeishu 函數(shù)是一個(gè)外部依賴。
另外,傳遞個(gè)給 sendFeishu 函數(shù)的 webhook 地址是從環(huán)境變量中獲取的,這也算是一個(gè)外部依賴。
所以要測(cè)試 monitor 函數(shù),我們需要使用測(cè)試替身來解決這兩個(gè)外部依賴項(xiàng)。
對(duì)于環(huán)境變量的依賴很好解決,Go 提供了 os.Setenv 可以在程序中動(dòng)態(tài)設(shè)置環(huán)境變量的值。
對(duì)于另一個(gè)依賴項(xiàng) sendFeishu 函數(shù),它又依賴了 webhook 地址所對(duì)應(yīng)的 HTTP Server。
所以我們需要解決 HTTP Server 的依賴問題。
針對(duì) HTTP Server,Go 標(biāo)準(zhǔn)庫 net/http/httptest 同樣提供了對(duì)應(yīng)工具。
我們可以使用 httptest.NewServer 創(chuàng)建一個(gè)測(cè)試用的 HTTP Server:
func newTestServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.RequestURI {
case "/success":
_, _ = fmt.Fprintf(w, `{"StatusCode":0,"StatusMessage":"success","code":0,"data":{},"msg":"success"}`)
case "/error":
_, _ = fmt.Fprintf(w, `{"code":19001,"data":{},"msg":"param invalid: incoming webhook access token invalid"}`)
}
}))
}newTestServer 函數(shù)返回一個(gè)用于測(cè)試的 HTTP Server 對(duì)象。
在 newTestServer 函數(shù)內(nèi)部,定義了兩個(gè)路由 /success 和 /error,分別來處理成功響應(yīng)和失敗響應(yīng)兩種情況。
與前文介紹的 setupTestUser 函數(shù)一樣,我們需要在測(cè)試程序開始執(zhí)行時(shí)準(zhǔn)備測(cè)試數(shù)據(jù),即啟動(dòng)這個(gè)測(cè)試用的 HTTP Server,在測(cè)試程序執(zhí)行完成后清理數(shù)據(jù),即關(guān)閉 HTTP Server。
不過,這次我們不再使用 setupTestUser 函數(shù)結(jié)合 defer cleanup() 的方式,而是換種方式來實(shí)現(xiàn):
var ts *httptest.Server
func TestMain(m *testing.M) {
ts = newTestServer()
m.Run()
ts.Close()
}首先我們定義了一個(gè)全局變量 ts,用來保存測(cè)試用的 HTTP Server 對(duì)象。
然后在 TestMain 函數(shù)中調(diào)用 newTestServer 函數(shù)為 ts 變量賦值。
接下來執(zhí)行 m.Run() 方法。
最終調(diào)用 ts.Close() 關(guān)閉 HTTP Server。
TestMain 函數(shù)名不是隨意取的,而是 Go 單元測(cè)試中的一個(gè)約定名稱,它相當(dāng)于 main 函數(shù),在使用 go test 命令執(zhí)行所有測(cè)試用例前,會(huì)優(yōu)先執(zhí)行 TestMain 函數(shù)。
在 TestMain 函數(shù)中調(diào)用 m.Run(),(*testing.M).Run() 方法會(huì)執(zhí)行全部的測(cè)試用例。
當(dāng)所有測(cè)試用例執(zhí)行完成后,代碼才會(huì)執(zhí)行到 ts.Close()。
所以,相較于 setupTestUser 函數(shù)在每個(gè)測(cè)試函數(shù)內(nèi)部都要調(diào)用一次的用法,TestMain 函數(shù)更加省力。不過這也決定了二者適用場(chǎng)景不同。TestMain 函數(shù)粒度更大,作用于全部測(cè)試用例,setupTestUser 函數(shù)只作用于單個(gè)測(cè)試函數(shù)。
現(xiàn)在,我們已經(jīng)解決了 monitor 函數(shù)的依賴項(xiàng)問題。
為其編寫的單元測(cè)試如下:
func Test_monitor(t *testing.T) {
type args struct {
pid int
webhook string
}
tests := []struct {
name string
args args
want *Result
wantErr error
}{
{
name: "process exited and send feishu success",
args: args{
pid: 10000000,
webhook: ts.URL + "/success",
},
want: &Result{
StatusCode: 0,
StatusMessage: "success",
Code: 0,
Data: make(map[string]interface{}),
Msg: "success",
},
},
{
name: "process exited and send feishu error",
args: args{
pid: 20000000,
webhook: ts.URL + "/error",
},
wantErr: errors.New("code: 19001, error: param invalid: incoming webhook access token invalid"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_ = os.Setenv("WEBHOOK", tt.args.webhook)
got, err := monitor(tt.args.pid)
if err != nil {
if tt.wantErr == nil || err.Error() != tt.wantErr.Error() {
t.Errorf("monitor() error = %v, wantErr %v", err, tt.wantErr)
return
}
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("monitor() got = %v, want %v", got, tt.want)
}
})
}
}這里同樣采用表格測(cè)試的方式,有兩個(gè)測(cè)試用例,一個(gè)用于測(cè)試被檢測(cè)程序退出后發(fā)送飛書消息成功的情況,一個(gè)用于測(cè)試被檢測(cè)程序退出后發(fā)送飛書消息失敗的情況。
測(cè)試用例中 pid 被設(shè)置為很大的值,已經(jīng)超過了 Linux 系統(tǒng)允許的最大 pid 值,所以檢測(cè)結(jié)果一定是程序已經(jīng)退出。
由于被檢測(cè)程序不退出的情況,monitor 函數(shù)會(huì)一直循環(huán)檢測(cè),邏輯比較簡(jiǎn)單,就沒有對(duì)這個(gè)邏輯編寫測(cè)試用例。
使用 go test 來執(zhí)行測(cè)試函數(shù):
$ go test -v -run="^Test_monitor$" .
=== RUN Test_monitor
=== RUN Test_monitor/process_exited_and_send_feishu_success
2023/07/15 13:27:46 Process 10000000 exited
=== RUN Test_monitor/process_exited_and_send_feishu_error
2023/07/15 13:27:46 Process 20000000 exited
--- PASS: Test_monitor (0.00s)
--- PASS: Test_monitor/process_exited_and_send_feishu_success (0.00s)
--- PASS: Test_monitor/process_exited_and_send_feishu_error (0.00s)
PASS
ok github.com/jianghushinian/blog-go-example/test/http/client 0.166s測(cè)試通過。
以上,我們通過 net/http/httptest 提供的測(cè)試工具,在本地啟動(dòng)了一個(gè)測(cè)試 HTTP Server,來解決被測(cè)試代碼依賴外部 HTTP 服務(wù)的問題。
有時(shí)候,我們不想真正的在本地啟動(dòng)一個(gè) HTTP Server,或者無法做到這一點(diǎn)。
那么,我們還有另一種方案來解決這個(gè)問題,可以使用 gock 來模擬 HTTP 服務(wù)。
gock 是 Go 社區(qū)中的一個(gè)第三方包,雖然不在本地啟動(dòng)一個(gè) HTTP Server,但是它能夠攔截所有被 mock 的 HTTP 請(qǐng)求。所以,我們能夠利用 gock 攔截 sendFeishu 函數(shù)發(fā)送給 webhook 地址的請(qǐng)求,然后返回 mock 數(shù)據(jù)。這樣,就可以使用 mock 的方式來解決依賴外部 HTTP 服務(wù)的問題。
使用 gock 編寫的單元測(cè)試代碼如下:
package main
import (
"os"
"testing"
"github.com/h2non/gock"
"github.com/stretchr/testify/assert"
)
func Test_monitor_by_gock(t *testing.T) {
defer gock.Off() // Flush pending mocks after test execution
gock.New("http://localhost:8080").
Post("/webhook").
Reply(200).
JSON(map[string]interface{}{
"StatusCode": 0,
"StatusMessage": "success",
"Code": 0,
"Data": make(map[string]interface{}),
"Msg": "success",
})
_ = os.Setenv("WEBHOOK", "http://localhost:8080/webhook")
got, err := monitor(30000000)
assert.NoError(t, err)
assert.Equal(t, &Result{
StatusCode: 0,
StatusMessage: "success",
Code: 0,
Data: make(map[string]interface{}),
Msg: "success",
}, got)
assert.True(t, gock.IsDone())
}首先,在測(cè)試函數(shù)的開始,使用 defer 延遲調(diào)用 gock.Off(),可以保證在測(cè)試完成后刷新掛起的 mock,即還原被 mock 對(duì)象的初始狀態(tài)。
然后,我們使用 gock.New() 對(duì) http://localhost:8080 這個(gè) URL 進(jìn)行 mock,這樣 gock 會(huì)攔截測(cè)試過程中所有發(fā)送到這個(gè)地址的 HTTP 請(qǐng)求。
gock.New() 支持鏈?zhǔn)秸{(diào)用,.Post("/webhook") 表示攔截對(duì) /webhook 這個(gè) URL 的 POST 請(qǐng)求。
.Reply(200) 表示針對(duì)這個(gè)請(qǐng)求,返回 200 狀態(tài)碼。
.JSON(...) 即為返回的 JSON 格式響應(yīng)內(nèi)容。
接著,我們將 webhook 地址設(shè)置為 http://localhost:8080/webhook,這樣,在調(diào)用 sendFeishu 函數(shù)時(shí)發(fā)送的請(qǐng)求就會(huì)被攔截,并返回上一步中的 .JSON(...) 內(nèi)容。
之后就是調(diào)用 monitor 函數(shù),并斷言測(cè)試結(jié)果是否正確。
最后,調(diào)用 assert.True(t, gock.IsDone()) 來驗(yàn)證已經(jīng)沒有掛起的 mock 了。
使用 go test 來執(zhí)行測(cè)試函數(shù):
$ go test -v -run="^Test_monitor_by_gock$" . === RUN Test_monitor_by_gock 2023/07/15 13:28:22 Process 30000000 exited --- PASS: Test_monitor_by_gock (0.00s) PASS ok github.com/jianghushinian/blog-go-example/test/http/client 0.574s
單元測(cè)試執(zhí)行通過。
總結(jié)
本文向大家介紹了在 Go 中編寫單元測(cè)試時(shí),如何解決 HTTP 外部依賴的問題。
我們分別站在 HTTP 服務(wù)端和 HTTP 客戶端兩個(gè)角度,使用 net/http/httptest 標(biāo)準(zhǔn)庫和 gock 第三方庫來實(shí)現(xiàn)測(cè)試替身解決 HTTP 外部依賴。
并且分別介紹了使用 setupTestUser + defer cleanup() 以及 TestMain 兩種形式,來做測(cè)試準(zhǔn)備和清理工作。二者作用于不同粒度,需要根據(jù)測(cè)試需要進(jìn)行選擇。
本文完整代碼示例:blog-go-example/test/http at main · jianghushinian/blog-go-example · GitHub
以上就是在Go語言單元測(cè)試中解決HTTP網(wǎng)絡(luò)依賴問題的詳細(xì)內(nèi)容,更多關(guān)于Go HTTP網(wǎng)絡(luò)依賴的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
go強(qiáng)制類型轉(zhuǎn)換type(a)以及范圍引起的數(shù)據(jù)差異
這篇文章主要為大家介紹了go強(qiáng)制類型轉(zhuǎn)換type(a)以及范圍引起的數(shù)據(jù)差異,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
golang開發(fā)微框架Gin的安裝測(cè)試及簡(jiǎn)介
這篇文章主要為大家介紹了golang微框架Gin的安裝測(cè)試及簡(jiǎn)介,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2021-11-11
Golang?依賴注入經(jīng)典解決方案uber/fx理論解析
這篇文章主要為大家介紹了Golang依賴注入經(jīng)典解決方案uber/fx理論解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
Go語言怎么使用變長(zhǎng)參數(shù)函數(shù)
本文主要介紹了Go語言怎么使用變長(zhǎng)參數(shù)函數(shù),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07

