Go 單元測試之HTTP請求與API測試

来源:https://www.cnblogs.com/taoxiaoxin/p/18141253
-Advertisement-
Play Games

目錄一、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 的基本步驟如下:

  1. 導入 net/http/httptest 包。
  2. 創建一個 httptest.Server 實例,並指定你想要的伺服器行為。
  3. 在測試代碼中使用 httptest.NewRequest 創建一個模擬的 HTTP 請求,並將其發送到 httptest.Server
  4. 檢查響應內容或狀態碼是否符合預期。

以下是一個簡單的 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 函數定義了一個測試函數,用於測試用戶登錄功能的不同情況。

  1. testCases 列表定義了多個測試用例,每個測試用例包含了測試名稱、請求體、期望的 HTTP 狀態碼和期望的響應體內容。
  2. 使用 for 迴圈遍歷測試用例列表,每次迴圈創建一個新的測試子函數,併在其中模擬 HTTP 請求發送給登錄介面。
  3. 在每個測試子函數中,先創建一個 Gin 的預設上下文和用戶處理器 UserHandler,然後註冊路由並創建一個模擬的 HTTP 請求。
  4. 通過 httptest.NewRecorder() 創建一個響應記錄器,使用 server.ServeHTTP(resp, req) 處理模擬請求,得到響應結果。
  5. 最後使用斷言來驗證實際響應的 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 的基本用法如下:

  1. 啟動攔截器:在測試開始前,使用 gock.New 函數啟動攔截器,並指定你想要攔截的功能變數名稱和埠。
  2. 定義攔截規則:你可以使用 gock.Intercept 方法來定義攔截規則,比如攔截特定的 URL、方法、頭部信息等。
  3. 設置響應:你可以使用 gock.NewJsongock.NewText 等方法來設置攔截後的響應內容。
  4. 運行測試:在定義了攔截規則和響應後,你可以運行測試,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被觸發
}
分享是一種快樂,開心是一種態度!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • splice在英語中的意思是拼接,在實際的代碼使用中,splice就在數組中起到了一個拼接的作用 使用方法 splice(x,y,a,b,c,...) 其中x、y為數字,a、b、c為新添加的項,意思是從數組的第x項開始刪除y項,併在其中添加a、b、c...,其中x、y必填,abc可不填 圖像理解 現 ...
  • 前言 2018年剛入行前端時,公司使用的還是Angular。Angular什麼都好,就是寫代碼時的體驗老糟心了,改一個地方,按下保存之後,要等好幾秒刷新後才能看到效果,Webstorm無比好用的自動保存,對我來說反而像是一個負擔。然而2024年了,Angular已經更新了17版本,還是沒有解決這個問 ...
  • 這兩天為了重啟五年前基於 React Native(版本 0.59.9)開發的老項目,經過各種填坑查詢等操作,最終把它成功地運行起來了。 在這篇文章中,我將詳述那些遭遇的挑戰以及對應的解決方案,以期為同樣面臨此類困境的開發者提供寶貴的經驗參考。 這個項目涉及到的環境基本版本信息如下: react: ...
  • 引言: 近年來,隨著教育理念的提升,對學生綜合素質的教育越發重視,特別是越發重視學生的身體素質提升,各階段的升學考試也將體測納入考核範圍。學校也推出了各種體測鍛煉促進手段,今天為您介紹一個基於小程式的,線上AI體測訓練打卡、評測方案。 一、體測功能需求 根據相關學生體測標準,體測小程式需要具備以下功 ...
  • 在網上一直流傳著一個爭論不休的話題:金額到底是用Long還是用BigDecimal?這個話題一齣在哪都會引起異常無比激烈的討論。。。。 比如說這個觀點:算錢用BigDecimal是常識 有支持用Long的,將金額的單位設計為分,然後乘以100,使用Long進行存儲以及計算,這樣不用擔心小數點問題。 ...
  • 用python開發的小紅書關鍵詞搜索軟體,採集欄位包含:關鍵詞, 頁碼, 筆記id, 筆記鏈接, 筆記標題, 筆記類型, 點贊數, 用戶id, 用戶主頁鏈接, 用戶昵稱。 ...
  • 目錄一、 sqlmock介紹二、安裝三、基本用法四、一個小案例五、Gorm 初始化註意點 一、 sqlmock介紹 sqlmock 是一個用於測試資料庫交互的 Go 模擬庫。它可以模擬 SQL 查詢、插入、更新等操作,並且可以驗證 SQL 語句的執行情況,非常適合用於單元測試中。 二、安裝 go g ...
  • shell腳本中的運算符和條件判斷: 一、算術運算符 在Shell腳本中,你可以使用各種運算符來執行數學運算、比較和邏輯操作。 計算方式: $[ ] $(( )) 例: a=$[(9+5)90] 列印輸出結果 ==> echo $a 二、條件判斷 判斷方式: test $a = 90 [ $a = ...
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...