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
  • PasteSpider是什麼? 一款使用.net編寫的開源的Linux容器部署助手,支持一鍵發佈,平滑升級,自動伸縮, Key-Value配置,項目網關,環境隔離,運行報表,差量升級,私有倉庫,集群部署,版本管理等! 30分鐘上手,讓開發也可以很容易的學會在linux上部署你得項目! [從需求角度介 ...
  • SQLSugar是什麼 **1. 輕量級ORM框架,專為.NET CORE開發人員設計,它提供了簡單、高效的方式來處理資料庫操作,使開發人員能夠更輕鬆地與資料庫進行交互 2. 簡化資料庫操作和數據訪問,允許開發人員在C#代碼中直接操作資料庫,而不需要編寫複雜的SQL語句 3. 支持多種資料庫,包括但 ...
  • 在C#中,經常會有一些耗時較長的CPU密集型運算,因為如果直接在UI線程執行這樣的運算就會出現UI不響應的問題。解決這類問題的主要途徑是使用多線程,啟動一個後臺線程,把運算操作放在這個後臺線程中完成。但是原生介面的線程操作有一些難度,如果要更進一步的去完成線程間的通訊就會難上加難。 因此,.NET類 ...
  • 一:背景 1. 講故事 前些天有位朋友在微信上丟了一個崩潰的dump給我,讓我幫忙看下為什麼出現了崩潰,在 Windows 的事件查看器上顯示的是經典的 訪問違例 ,即 c0000005 錯誤碼,不管怎麼說有dump就可以上windbg開幹了。 二:WinDbg 分析 1. 程式為誰崩潰了 在 Wi ...
  • CSharpe中的IO+NPOI+序列化 文件文件夾操作 學習一下常見的文件、文件夾的操作。 什麼是IO流? I:就是input O:就是output,故稱:輸入輸出流 將數據讀入記憶體或者記憶體輸出的過程。 常見的IO流操作,一般說的是[記憶體]與[磁碟]之間的輸入輸出。 作用 持久化數據,保證數據不再 ...
  • C#.NET與JAVA互通之MD5哈希V2024 配套視頻: 要點: 1.計算MD5時,SDK自帶的計算哈希(ComputeHash)方法,輸入輸出參數都是byte數組。就涉及到字元串轉byte數組轉換時,編碼選擇的問題。 2.輸入參數,字元串轉byte數組時,編碼雙方要統一,一般為:UTF-8。 ...
  • CodeWF.EventBus,一款靈活的事件匯流排庫,實現模塊間解耦通信。支持多種.NET項目類型,如WPF、WinForms、ASP.NET Core等。採用簡潔設計,輕鬆實現事件的發佈與訂閱。通過有序的消息處理,確保事件得到妥善處理。簡化您的代碼,提升系統可維護性。 ...
  • 一、基本的.NET框架概念 .NET框架是一個由微軟開發的軟體開發平臺,它提供了一個運行時環境(CLR - Common Language Runtime)和一套豐富的類庫(FCL - Framework Class Library)。CLR負責管理代碼的執行,而FCL則提供了大量預先編寫好的代碼, ...
  • 本章將和大家分享在ASP.NET Core中如何使用高級客戶端NEST來操作我們的Elasticsearch。 NEST是一個高級別的Elasticsearch .NET客戶端,它仍然非常接近原始Elasticsearch API的映射。所有的請求和響應都是通過類型來暴露的,這使得它非常適合快速上手 ...
  • 參考delphi的代碼更改為C# Delphi 檢測密碼強度 規則(仿 google) 仿 google 評分規則 一、密碼長度: 5 分: 小於等於 4 個字元 10 分: 5 到 7 字元 25 分: 大於等於 8 個字元 二、字母: 0 分: 沒有字母 10 分: 全都是小(大)寫字母 20 ...