欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

在Go語言單元測試中解決HTTP網(wǎng)絡(luò)依賴問題

 更新時(shí)間:2023年07月24日 11:21:42   作者:江湖十年  
在 Go 語言中,我們需要找到一種可靠的方法來測試 HTTP 請求和響應(yīng),本文將探討在 Go 中進(jìn)行 HTTP 應(yīng)用測試時(shí),如何解決應(yīng)用程序的依賴問題,以確保我們能夠編寫出可靠的測試用例,需要的朋友可以參考下

在開發(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ù)的正確性,我們需要對 CreateUserHandlerGetUserHandler 這兩個(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 方法返回 400500 兩種狀態(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)文章

  • Goland編輯器設(shè)置選擇范圍背景色的操作

    Goland編輯器設(shè)置選擇范圍背景色的操作

    這篇文章主要介紹了Goland編輯器設(shè)置選擇范圍背景色的操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • Go語言寫入字符串到文件的方法

    Go語言寫入字符串到文件的方法

    這篇文章主要介紹了Go語言寫入字符串到文件的方法,實(shí)例分析了Go語言操作字符串及文本的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-02-02
  • golang并發(fā)ping主機(jī)的方法

    golang并發(fā)ping主機(jī)的方法

    今天小編就為大家分享一篇golang并發(fā)ping主機(jī)的方法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2019-07-07
  • golang如何優(yōu)雅的編寫事務(wù)代碼示例

    golang如何優(yōu)雅的編寫事務(wù)代碼示例

    這篇文章主要介紹了golang如何優(yōu)雅的編寫事務(wù)代碼示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-05-05
  • go強(qiáng)制類型轉(zhuǎn)換type(a)以及范圍引起的數(shù)據(jù)差異

    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的安裝測試及簡介

    golang開發(fā)微框架Gin的安裝測試及簡介

    這篇文章主要為大家介紹了golang微框架Gin的安裝測試及簡介,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪
    2021-11-11
  • Golang中的http.Server源碼深入分析

    Golang中的http.Server源碼深入分析

    這篇文章主要介紹了Golang中的http.Server源碼,實(shí)現(xiàn)一個(gè)http.Server非常容易,只需要短短幾行代碼,同時(shí)有了協(xié)程的加持,Go實(shí)現(xiàn)的http.Server能夠取得非常優(yōu)秀的性能,下面我們來分析看看http.Server的源碼
    2023-05-05
  • Golang?依賴注入經(jīng)典解決方案uber/fx理論解析

    Golang?依賴注入經(jīng)典解決方案uber/fx理論解析

    這篇文章主要為大家介紹了Golang依賴注入經(jīng)典解決方案uber/fx理論解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-05-05
  • 基于Go語言開發(fā)一個(gè)編解碼工具

    基于Go語言開發(fā)一個(gè)編解碼工具

    這篇文章主要為大家詳細(xì)介紹了如何基于Go語言開發(fā)一個(gè)編解碼工具,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以跟隨小編一起了解一下
    2025-03-03
  • Go語言怎么使用變長參數(shù)函數(shù)

    Go語言怎么使用變長參數(shù)函數(shù)

    本文主要介紹了Go語言怎么使用變長參數(shù)函數(shù),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-07-07

最新評論