写多了测试代码,你会发现很多重复的模式:创建请求、解析响应、设置请求头等等。把这些操作封装成工具函数,能让测试代码更简洁、更易维护。
package testutils
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"github.com/gin-gonic/gin"
)
// 设置测试模式
func init() {
gin.SetMode(gin.TestMode)
}
// PerformGET 发送 GET 请求
func PerformGET(router *gin.Engine, path string) *httptest.ResponseRecorder {
req := httptest.NewRequest("GET", path, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
// PerformGETWithHeaders 发送带请求头的 GET 请求
func PerformGETWithHeaders(router *gin.Engine, path string, headers map[string]string) *httptest.ResponseRecorder {
req := httptest.NewRequest("GET", path, nil)
for key, value := range headers {
req.Header.Set(key, value)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
// PerformPOST 发送 JSON POST 请求
func PerformPOST(router *gin.Engine, path string, body interface{}) *httptest.ResponseRecorder {
jsonBody, _ := json.Marshal(body)
req := httptest.NewRequest("POST", path, bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
// PerformPOSTRaw 发送原始 body 的 POST 请求
func PerformPOSTRaw(router *gin.Engine, path string, body string) *httptest.ResponseRecorder {
req := httptest.NewRequest("POST", path, bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
// PerformPUT 发送 PUT 请求
func PerformPUT(router *gin.Engine, path string, body interface{}) *httptest.ResponseRecorder {
jsonBody, _ := json.Marshal(body)
req := httptest.NewRequest("PUT", path, bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
// PerformDELETE 发送 DELETE 请求
func PerformDELETE(router *gin.Engine, path string) *httptest.ResponseRecorder {
req := httptest.NewRequest("DELETE", path, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
// ParseResponse 解析 JSON 响应到目标结构
func ParseResponse(w *httptest.ResponseRecorder, target interface{}) error {
return json.Unmarshal(w.Body.Bytes(), target)
}
// ParseResponseMap 解析 JSON 响应为 map
func ParseResponseMap(w *httptest.ResponseRecorder) map[string]interface{} {
var result map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &result)
return result
}
// GetResponseBody 获取响应体字符串
func GetResponseBody(w *httptest.ResponseRecorder) string {
return w.Body.String()
}
import "testing"
// AssertStatus 断言状态码
func AssertStatus(t *testing.T, w *httptest.ResponseRecorder, expected int) {
t.Helper()
if w.Code != expected {
t.Errorf("Expected status %d, got %d. Body: %s", expected, w.Code, w.Body.String())
}
}
// AssertJSON 断言 JSON 响应
func AssertJSON(t *testing.T, w *httptest.ResponseRecorder, expected string) {
t.Helper()
if w.Body.String() != expected {
t.Errorf("Expected body %s, got %s", expected, w.Body.String())
}
}
// AssertContains 断言响应包含特定字符串
func AssertContains(t *testing.T, w *httptest.ResponseRecorder, substr string) {
t.Helper()
if !bytes.Contains(w.Body.Bytes(), []byte(substr)) {
t.Errorf("Response body does not contain %s. Body: %s", substr, w.Body.String())
}
}
// AssertHeader 断言响应头
func AssertHeader(t *testing.T, w *httptest.ResponseRecorder, key, expected string) {
t.Helper()
actual := w.Header().Get(key)
if actual != expected {
t.Errorf("Expected header %s=%s, got %s", key, expected, actual)
}
}
把上面的函数组织成一个完整的测试工具包:
package testutils
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func init() {
gin.SetMode(gin.TestMode)
}
type RequestConfig struct {
Method string
Path string
Body interface{}
Headers map[string]string
Cookies []*http.Cookie
}
// Request 发送自定义请求
func Request(router *gin.Engine, config RequestConfig) *httptest.ResponseRecorder {
var bodyReader *bytes.Buffer
if config.Body != nil {
switch v := config.Body.(type) {
case string:
bodyReader = bytes.NewBufferString(v)
case []byte:
bodyReader = bytes.NewBuffer(v)
default:
jsonBody, _ := json.Marshal(v)
bodyReader = bytes.NewBuffer(jsonBody)
}
} else {
bodyReader = bytes.NewBuffer(nil)
}
req := httptest.NewRequest(config.Method, config.Path, bodyReader)
// 设置请求头
if config.Body != nil {
req.Header.Set("Content-Type", "application/json")
}
for key, value := range config.Headers {
req.Header.Set(key, value)
}
// 设置 Cookie
for _, cookie := range config.Cookies {
req.AddCookie(cookie)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
// 快捷方法
func GET(router *gin.Engine, path string, headers ...map[string]string) *httptest.ResponseRecorder {
h := make(map[string]string)
if len(headers) > 0 {
h = headers[0]
}
return Request(router, RequestConfig{
Method: "GET",
Path: path,
Headers: h,
})
}
func POST(router *gin.Engine, path string, body interface{}, headers ...map[string]string) *httptest.ResponseRecorder {
h := make(map[string]string)
if len(headers) > 0 {
h = headers[0]
}
return Request(router, RequestConfig{
Method: "POST",
Path: path,
Body: body,
Headers: h,
})
}
func PUT(router *gin.Engine, path string, body interface{}, headers ...map[string]string) *httptest.ResponseRecorder {
h := make(map[string]string)
if len(headers) > 0 {
h = headers[0]
}
return Request(router, RequestConfig{
Method: "PUT",
Path: path,
Body: body,
Headers: h,
})
}
func DELETE(router *gin.Engine, path string, headers ...map[string]string) *httptest.ResponseRecorder {
h := make(map[string]string)
if len(headers) > 0 {
h = headers[0]
}
return Request(router, RequestConfig{
Method: "DELETE",
Path: path,
Headers: h,
})
}
有了工具函数后,测试代码变得简洁:
package main
import (
"net/http"
"testing"
"github.com/gin-gonic/gin"
"your-project/testutils"
)
func TestUserAPI(t *testing.T) {
router := setupRouter()
t.Run("Create user", func(t *testing.T) {
body := map[string]string{
"name": "Alice",
"email": "alice@example.com",
}
w := testutils.POST(router, "/users", body)
testutils.AssertStatus(t, w, http.StatusCreated)
var response map[string]interface{}
testutils.ParseResponse(w, &response)
if response["name"] != "Alice" {
t.Errorf("Expected name Alice, got %v", response["name"])
}
})
t.Run("Get user", func(t *testing.T) {
w := testutils.GET(router, "/users/1")
testutils.AssertStatus(t, w, http.StatusOK)
})
t.Run("Update user", func(t *testing.T) {
body := map[string]string{
"name": "Bob",
}
w := testutils.PUT(router, "/users/1", body)
testutils.AssertStatus(t, w, http.StatusOK)
})
t.Run("Delete user", func(t *testing.T) {
w := testutils.DELETE(router, "/users/1")
testutils.AssertStatus(t, w, http.StatusNoContent)
})
}
func setupRouter() *gin.Engine {
router := gin.New()
router.POST("/users", func(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{"id": 1, "name": "Alice"})
})
router.GET("/users/:id", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"id": 1, "name": "Alice"})
})
router.PUT("/users/:id", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"id": 1, "name": "Bob"})
})
router.DELETE("/users/:id", func(c *gin.Context) {
c.Status(http.StatusNoContent)
})
return router
}
如果 API 需要认证,可以封装认证相关的辅助函数:
// WithAuth 添加认证头
func WithAuth(token string) map[string]string {
return map[string]string{
"Authorization": "Bearer " + token,
}
}
// GetAuthToken 获取测试用的认证 token
func GetAuthToken(router *gin.Engine, username, password string) string {
body := map[string]string{
"username": username,
"password": password,
}
w := POST(router, "/login", body)
var response map[string]interface{}
ParseResponse(w, &response)
return response["token"].(string)
}
// AuthenticatedRequest 发送认证请求
func AuthenticatedRequest(router *gin.Engine, method, path string, body interface{}, token string) *httptest.ResponseRecorder {
return Request(router, RequestConfig{
Method: method,
Path: path,
Body: body,
Headers: WithAuth(token),
})
}
使用示例:
func TestProtectedAPI(t *testing.T) {
router := setupRouter()
// 获取 token
token := testutils.GetAuthToken(router, "testuser", "testpass")
// 使用认证请求
w := testutils.AuthenticatedRequest(router, "GET", "/protected", nil, token)
testutils.AssertStatus(t, w, http.StatusOK)
}
如果测试需要数据库,可以封装数据库相关的辅助函数:
package testutils
import (
"database/sql"
"os"
"testing"
_ "github.com/lib/pq"
)
var testDB *sql.DB
// SetupDB 初始化测试数据库
func SetupDB(t *testing.T) *sql.DB {
t.Helper()
dsn := os.Getenv("TEST_DATABASE_URL")
if dsn == "" {
dsn = "postgres://test:test@localhost:5432/test_db?sslmode=disable"
}
db, err := sql.Open("postgres", dsn)
if err != nil {
t.Fatalf("Failed to connect database: %v", err)
}
// 清理表
db.Exec("TRUNCATE users CASCADE")
return db
}
// TeardownDB 清理测试数据库
func TeardownDB(t *testing.T, db *sql.DB) {
t.Helper()
db.Exec("TRUNCATE users CASCADE")
db.Close()
}
// InsertTestUser 插入测试用户
func InsertTestUser(t *testing.T, db *sql.DB, name, email string) int {
t.Helper()
var id int
err := db.QueryRow(
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
name, email,
).Scan(&id)
if err != nil {
t.Fatalf("Failed to insert test user: %v", err)
}
return id
}
测试工具函数能显著提高测试效率:
t.Helper() 标记辅助函数,使错误报告更准确好的测试工具函数让测试代码更易读、更易维护,也更容易让团队成员写出一致的测试代码。建议在项目中建立自己的测试工具库,随着项目发展不断完善。