在Go語言單元測試中解決HTTP網(wǎng)絡(luò)依賴問題
在開發(fā) Web 應(yīng)用程序時(shí),確保 HTTP 功能的正確性是至關(guān)重要的。然而,由于 Web 應(yīng)用程序通常涉及到與外部依賴的交互,編寫 HTTP 請求和響應(yīng)的有效測試變得具有挑戰(zhàn)性。在進(jìn)行單元測試時(shí),我們必須思考如何解決被測程序的外部依賴問題。
HTTP Server 測試
首先,我們來看下,站在 HTTP Server 端的角度,如何編寫應(yīng)用程序的測試代碼。
假設(shè)我們有一個(gè) HTTP Server 對外提供服務(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 對應(yīng)的用戶信息。
為了保證業(yè)務(wù)的正確性,我們需要對 CreateUserHandler
和 GetUserHandler
這兩個(gè) Handler 進(jìn)行單元測試。
我們先來看下用于創(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 格式。
接著從請求體 r.Body
中讀取客戶端提交的用戶信息。
如果讀取請求體失敗,則寫入響應(yīng)狀態(tài)碼 400
,表示客戶端提交的用戶信息有誤,并返回 JSON 錯(cuò)誤響應(yīng)。
接著,使用 json.Unmarshal
對請求體進(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)體。
下面,我們來分析下如何對這個(gè) Handler 函數(shù)編寫單元測試代碼。
首先,我們思考下 CreateUserHandler
這個(gè)函數(shù)都有哪些外部依賴?
從函數(shù)參數(shù)來看,我們需要一個(gè)用來表示 HTTP 響應(yīng)的 http.ResponseWriter
,一個(gè)用來表示 HTTP 請求的 *http.Request
,以及一個(gè)用來記錄 HTTP 請求路由參數(shù)的 httprouter.Params
。
在函數(shù)內(nèi)部,則依賴了全局變量 users
。
知道了這些外部依賴,那么,我們?nèi)绾尉帉憜卧獪y試才能解決這些外部依賴呢?
最直接的辦法,就是啟動(dòng)這個(gè) Web Server,然后在單元測試代碼中對 POST /users
接口發(fā)送一個(gè) HTTP 請求,之后判斷程序的 HTTP 響應(yīng)結(jié)果以及 users
變量中的數(shù)據(jù),來驗(yàn)證 CreateUserHandler
函數(shù)的正確性。
但這種做法顯然超出了單元測試的范疇,更像是在做集成測試。單元測試的一個(gè)主要特征就是要隔離外部依賴,使用測試替身
來替換依賴。
所以,我們應(yīng)該想辦法來制作測試替身
。
我們先從最簡單的 users
變量開始,想辦法在測試過程中替換掉 users
。
users
僅是一個(gè)切片變量,用來保存用戶數(shù)據(jù),我們可以編寫一個(gè)函數(shù),將其內(nèi)容替換成測試數(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ù)。
在測試期間可以這樣使用:
func TestCreateUserHandler(t *testing.T) { cleanup := setupTestUser() defer cleanup() ... }
在測試最開始時(shí)調(diào)用 setupTestUser
來初始化測試數(shù)據(jù),使用 defer
語句實(shí)現(xiàn)測試函數(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ò)測試的實(shí)用工具。
構(gòu)造一個(gè)測試用的 HTTP 響應(yīng)對象僅需一行代碼就能完成:
w := httptest.NewRecorder()
得到的 w
變量實(shí)現(xiàn)了 http.ResponseWriter
接口,可以直接傳遞給 Handler 函數(shù)。
要想構(gòu)造一個(gè)表示 HTTP 請求的 *http.Request
對象,同樣非常簡單:
body := strings.NewReader(`{"name": "user2"}`) req := httptest.NewRequest("POST", "/users", body)
使用 httptest.NewRequest
創(chuàng)建的 req
變量正是 *http.Request
類型,它包含了請求方法、路徑、請求體。
現(xiàn)在,我們只差一個(gè)用來記錄 HTTP 請求路由參數(shù)的 httprouter.Params
類型對象沒有構(gòu)造了。
httprouter.Params
是由 httprouter
這個(gè)第三方包提供的,httprouter
是一個(gè)高性能的 HTTP 路由,兼容 net/http
標(biāo)準(zhǔn)庫。
它提供了 (*httprouter.Router).ServeHTTP
方法,可以調(diào)用請求對應(yīng)的 Handler 函數(shù)。即可以根據(jù)請求對象 *http.Request
,自動(dòng)調(diào)用 CreateUserHandler
函數(shù)。
在調(diào)用 Handler 函數(shù)時(shí),httprouter
會解析請求中的路由參數(shù)保存在 httprouter.Params
對象中并傳給 Handler,所以這個(gè)對象無需我們手動(dòng)構(gòu)造。
現(xiàn)在,單元測試函數(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)造了單元測試所需的依賴項(xiàng)。
setupRouter()
返回 *httprouter.Router
對象,當(dāng)代碼執(zhí)行到 router.ServeHTTP(w, req)
時(shí),就會根據(jù)傳遞的 req
參數(shù),自動(dòng)調(diào)用與之匹配的 Handler,即被測試函數(shù) CreateUserHandler
。
接下來,我們要做的就是判斷 CreateUserHandler
函數(shù)執(zhí)行后的結(jié)果是否正確。
完整單元測試代碼如下:
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è)對象是否相等,這可以簡化代碼,不再需要使用 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í)行測試函數(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
測試通過。
至此,我們成功為 CreateUserHandler
函數(shù)編寫了一個(gè)單元測試。
不過,這個(gè)單元測試僅覆蓋了正常邏輯,CreateUserHandler
方法返回 400
和 500
兩種狀態(tài)碼的邏輯沒有被測試覆蓋,這兩種場景就留做作業(yè)你自己來完成吧。
接下來,我們再為獲取用戶信息的函數(shù) GetUserHandler
編寫一個(gè)單元測試。
先來看下 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"}`)) }
獲取用戶信息的邏輯,相對簡單一點(diǎn)。
首先從 HTTP 請求的路徑參數(shù)中獲取用戶 ID。
然后判斷這個(gè) ID 對應(yīng)的用戶信息是否存在,如果存在就返回用戶信息。
不存在,則寫入 404
狀態(tài)碼,并返回 notfound
信息。
有了前文對 CreateUserHandler
函數(shù)編寫測試的經(jīng)驗(yàn),想必如何對 GetUserHandler
函數(shù)進(jìn)行測試你已經(jīng)輕車熟路了。
以下是我為其編寫的測試代碼:
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()) }) } }
獲取用戶信息的單元測試代碼,在測試執(zhí)行開始,同樣使用 setupTestUser
函數(shù)來初始化測試數(shù)據(jù),并使用 defer
來完成數(shù)據(jù)恢復(fù)。
這次為了提高測試覆蓋率,我對 GetUserHandler
函數(shù)的正常響應(yīng)以及返回 404
狀態(tài)碼的異常響應(yīng)場景都進(jìn)行了測試。
這里使用了表格測試,
除了使用表格測試的形式,其他測試邏輯與 CreateUserHandler
的單元測試邏輯基本相同,我就不過多介紹了。
使用 go test
來執(zhí)行測試函數(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
表格測試的兩個(gè)用例都通過了測試。
HTTP Client 測試
接下來,我們來看下,站在 HTTP Client 端的角度,如何編寫應(yīng)用程序的測試代碼。
假設(shè)我們有一個(gè)進(jìn)程監(jiān)控程序,能夠檢測某個(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),會根據(jù)傳遞進(jìn)來的進(jìn)程 PID 不斷的來檢測對應(yīng)進(jìn)程是否存在。
如果不存在,則說明進(jìn)程已經(jīng)停止,然后調(diào)用 sendFeishu
函數(shù)發(fā)送消息通知到指定的飛書 webhook
地址。
monitor
函數(shù)會將 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)作第三方包來使用即可,僅需要知道它最終會返回 *Result
對象。
現(xiàn)在我們需要對 monitor
函數(shù)進(jìn)行測試。
我們同樣需要先分析下 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è)外部依賴。
所以要測試 monitor
函數(shù),我們需要使用測試替身
來解決這兩個(gè)外部依賴項(xiàng)。
對于環(huán)境變量的依賴很好解決,Go 提供了 os.Setenv
可以在程序中動(dòng)態(tài)設(shè)置環(huán)境變量的值。
對于另一個(gè)依賴項(xiàng) sendFeishu
函數(shù),它又依賴了 webhook
地址所對應(yīng)的 HTTP Server。
所以我們需要解決 HTTP Server 的依賴問題。
針對 HTTP Server,Go 標(biāo)準(zhǔn)庫 net/http/httptest
同樣提供了對應(yīng)工具。
我們可以使用 httptest.NewServer
創(chuàng)建一個(gè)測試用的 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è)用于測試的 HTTP Server 對象。
在 newTestServer
函數(shù)內(nèi)部,定義了兩個(gè)路由 /success
和 /error
,分別來處理成功響應(yīng)和失敗響應(yīng)兩種情況。
與前文介紹的 setupTestUser
函數(shù)一樣,我們需要在測試程序開始執(zhí)行時(shí)準(zhǔn)備測試數(shù)據(jù),即啟動(dòng)這個(gè)測試用的 HTTP Server,在測試程序執(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
,用來保存測試用的 HTTP Server 對象。
然后在 TestMain
函數(shù)中調(diào)用 newTestServer
函數(shù)為 ts
變量賦值。
接下來執(zhí)行 m.Run()
方法。
最終調(diào)用 ts.Close()
關(guān)閉 HTTP Server。
TestMain
函數(shù)名不是隨意取的,而是 Go 單元測試中的一個(gè)約定名稱,它相當(dāng)于 main
函數(shù),在使用 go test
命令執(zhí)行所有測試用例前,會優(yōu)先執(zhí)行 TestMain
函數(shù)。
在 TestMain
函數(shù)中調(diào)用 m.Run()
,(*testing.M).Run()
方法會執(zhí)行全部的測試用例。
當(dāng)所有測試用例執(zhí)行完成后,代碼才會執(zhí)行到 ts.Close()
。
所以,相較于 setupTestUser
函數(shù)在每個(gè)測試函數(shù)內(nèi)部都要調(diào)用一次的用法,TestMain
函數(shù)更加省力。不過這也決定了二者適用場景不同。TestMain
函數(shù)粒度更大,作用于全部測試用例,setupTestUser
函數(shù)只作用于單個(gè)測試函數(shù)。
現(xiàn)在,我們已經(jīng)解決了 monitor
函數(shù)的依賴項(xiàng)問題。
為其編寫的單元測試如下:
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) } }) } }
這里同樣采用表格測試的方式,有兩個(gè)測試用例,一個(gè)用于測試被檢測程序退出后發(fā)送飛書消息成功的情況,一個(gè)用于測試被檢測程序退出后發(fā)送飛書消息失敗的情況。
測試用例中 pid
被設(shè)置為很大的值,已經(jīng)超過了 Linux 系統(tǒng)允許的最大 pid
值,所以檢測結(jié)果一定是程序已經(jīng)退出。
由于被檢測程序不退出的情況,monitor
函數(shù)會一直循環(huán)檢測,邏輯比較簡單,就沒有對這個(gè)邏輯編寫測試用例。
使用 go test
來執(zhí)行測試函數(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
測試通過。
以上,我們通過 net/http/httptest
提供的測試工具,在本地啟動(dòng)了一個(gè)測試 HTTP Server,來解決被測試代碼依賴外部 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 請求。所以,我們能夠利用 gock
攔截 sendFeishu
函數(shù)發(fā)送給 webhook
地址的請求,然后返回 mock 數(shù)據(jù)。這樣,就可以使用 mock 的方式來解決依賴外部 HTTP 服務(wù)的問題。
使用 gock
編寫的單元測試代碼如下:
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()) }
首先,在測試函數(shù)的開始,使用 defer
延遲調(diào)用 gock.Off()
,可以保證在測試完成后刷新掛起的 mock,即還原被 mock 對象的初始狀態(tài)。
然后,我們使用 gock.New()
對 http://localhost:8080
這個(gè) URL 進(jìn)行 mock,這樣 gock
會攔截測試過程中所有發(fā)送到這個(gè)地址的 HTTP 請求。
gock.New()
支持鏈?zhǔn)秸{(diào)用,.Post("/webhook")
表示攔截對 /webhook
這個(gè) URL 的 POST 請求。
.Reply(200)
表示針對這個(gè)請求,返回 200
狀態(tài)碼。
.JSON(...)
即為返回的 JSON 格式響應(yīng)內(nèi)容。
接著,我們將 webhook
地址設(shè)置為 http://localhost:8080/webhook
,這樣,在調(diào)用 sendFeishu
函數(shù)時(shí)發(fā)送的請求就會被攔截,并返回上一步中的 .JSON(...)
內(nèi)容。
之后就是調(diào)用 monitor
函數(shù),并斷言測試結(jié)果是否正確。
最后,調(diào)用 assert.True(t, gock.IsDone())
來驗(yàn)證已經(jīng)沒有掛起的 mock 了。
使用 go test
來執(zhí)行測試函數(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
單元測試執(zhí)行通過。
總結(jié)
本文向大家介紹了在 Go 中編寫單元測試時(shí),如何解決 HTTP 外部依賴的問題。
我們分別站在 HTTP 服務(wù)端和 HTTP 客戶端兩個(gè)角度,使用 net/http/httptest
標(biāo)準(zhǔn)庫和 gock
第三方庫來實(shí)現(xiàn)測試替身
解決 HTTP 外部依賴。
并且分別介紹了使用 setupTestUser
+ defer cleanup()
以及 TestMain
兩種形式,來做測試準(zhǔn)備和清理工作。二者作用于不同粒度,需要根據(jù)測試需要進(jìn)行選擇。
本文完整代碼示例:blog-go-example/test/http at main · jianghushinian/blog-go-example · GitHub
以上就是在Go語言單元測試中解決HTTP網(wǎng)絡(luò)依賴問題的詳細(xì)內(nèi)容,更多關(guān)于Go HTTP網(wǎng)絡(luò)依賴的資料請關(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-10Golang?依賴注入經(jīng)典解決方案uber/fx理論解析
這篇文章主要為大家介紹了Golang依賴注入經(jīng)典解決方案uber/fx理論解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05