Files
xlgo-core/test/test.go
T
2026-07-08 18:21:32 +08:00

427 lines
9.6 KiB
Go

package test
import (
"bytes"
"encoding/json"
"errors"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"sync"
"testing"
"github.com/gin-gonic/gin"
)
// SetupRouter 创建不带框架默认中间件的测试路由。
//
// 需要 RequestID、Recover、超时等框架中间件的集成测试,应直接构造 xlgo.App
// 或手动 Use 对应中间件。
func SetupRouter() *gin.Engine {
gin.SetMode(gin.TestMode)
return gin.New()
}
// Request 是测试请求构造器。
type Request struct {
router *gin.Engine
method string
path string
body any
headers map[string]string
}
// NewRequest 创建测试请求。
func NewRequest(router *gin.Engine, method, path string) *Request {
return &Request{
router: router,
method: method,
path: path,
headers: make(map[string]string),
}
}
// GET 创建 GET 请求。
func GET(router *gin.Engine, path string) *Request {
return NewRequest(router, http.MethodGet, path)
}
// POST 创建 POST 请求。
func POST(router *gin.Engine, path string) *Request {
return NewRequest(router, http.MethodPost, path)
}
// PUT 创建 PUT 请求。
func PUT(router *gin.Engine, path string) *Request {
return NewRequest(router, http.MethodPut, path)
}
// DELETE 创建 DELETE 请求。
func DELETE(router *gin.Engine, path string) *Request {
return NewRequest(router, http.MethodDelete, path)
}
// PATCH 创建 PATCH 请求。
func PATCH(router *gin.Engine, path string) *Request {
return NewRequest(router, http.MethodPatch, path)
}
// WithBody 设置请求体。
func (r *Request) WithBody(body any) *Request {
r.body = body
return r
}
// WithJSON 设置 JSON 请求体并写入 Content-Type。
func (r *Request) WithJSON(body any) *Request {
r.body = body
r.headers["Content-Type"] = "application/json"
return r
}
// WithHeader 设置请求头。
func (r *Request) WithHeader(key, value string) *Request {
r.headers[key] = value
return r
}
// WithToken 设置 Authorization Bearer token。
func (r *Request) WithToken(token string) *Request {
r.headers["Authorization"] = "Bearer " + token
return r
}
// Execute 执行请求并返回带断言方法的响应包装。
func (r *Request) Execute() *Response {
return &Response{ResponseRecorder: r.ExecuteRecorder()}
}
// ExecuteRecorder 执行请求并返回原始 httptest.ResponseRecorder。
func (r *Request) ExecuteRecorder() *httptest.ResponseRecorder {
var bodyReader *bytes.Reader
if r.body != nil {
bodyBytes, _ := json.Marshal(r.body)
bodyReader = bytes.NewReader(bodyBytes)
} else {
bodyReader = bytes.NewReader(nil)
}
req := httptest.NewRequest(r.method, r.path, bodyReader)
for key, value := range r.headers {
req.Header.Set(key, value)
}
recorder := httptest.NewRecorder()
r.router.ServeHTTP(recorder, req)
return recorder
}
// Response 是测试响应辅助包装。
type Response struct {
*httptest.ResponseRecorder
}
// ParseJSON 解析 JSON 响应。
func (r *Response) ParseJSON(v any) error {
return json.Unmarshal(r.Body.Bytes(), v)
}
// AssertStatus 断言 HTTP 状态码。
func (r *Response) AssertStatus(t *testing.T, expected int) {
t.Helper()
if r.Code != expected {
t.Errorf("状态码错误: 期望 %d, 实际 %d", expected, r.Code)
}
}
// AssertOK 断言状态码为 200。
func (r *Response) AssertOK(t *testing.T) {
t.Helper()
r.AssertStatus(t, http.StatusOK)
}
// AssertCreated 断言状态码为 201。
func (r *Response) AssertCreated(t *testing.T) {
t.Helper()
r.AssertStatus(t, http.StatusCreated)
}
// AssertBadRequest 断言状态码为 400。
func (r *Response) AssertBadRequest(t *testing.T) {
t.Helper()
r.AssertStatus(t, http.StatusBadRequest)
}
// AssertUnauthorized 断言状态码为 401。
func (r *Response) AssertUnauthorized(t *testing.T) {
t.Helper()
r.AssertStatus(t, http.StatusUnauthorized)
}
// AssertForbidden 断言状态码为 403。
func (r *Response) AssertForbidden(t *testing.T) {
t.Helper()
r.AssertStatus(t, http.StatusForbidden)
}
// AssertNotFound 断言状态码为 404。
func (r *Response) AssertNotFound(t *testing.T) {
t.Helper()
r.AssertStatus(t, http.StatusNotFound)
}
// AssertJSONContains 断言 JSON 响应包含指定字段和值,支持 data.user.id 形式的嵌套键。
func (r *Response) AssertJSONContains(t *testing.T, key string, expected any) {
t.Helper()
var result map[string]any
if err := r.ParseJSON(&result); err != nil {
t.Errorf("解析 JSON 失败: %v", err)
return
}
keys := splitKey(key)
current := result
for i, k := range keys {
if i == len(keys)-1 {
if current[k] != expected {
t.Errorf("JSON 字段 %s 错误: 期望 %v, 实际 %v", key, expected, current[k])
}
return
}
next, ok := current[k].(map[string]any)
if !ok {
t.Errorf("JSON 字段 %s 不是对象", k)
return
}
current = next
}
}
func splitKey(key string) []string {
var keys []string
start := 0
for i := 0; i < len(key); i++ {
if key[i] == '.' {
keys = append(keys, key[start:i])
start = i + 1
}
}
keys = append(keys, key[start:])
return keys
}
// MockDB 是并发安全的简单内存数据库 mock。
type MockDB struct {
mu sync.RWMutex
data map[any]any
}
// NewMockDB 创建模拟数据库。
func NewMockDB() *MockDB {
return &MockDB{data: make(map[any]any)}
}
// Set 设置数据。
func (m *MockDB) Set(key, value any) {
m.mu.Lock()
defer m.mu.Unlock()
m.data[key] = value
}
// Get 获取数据。
func (m *MockDB) Get(key any) (any, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
v, ok := m.data[key]
return v, ok
}
// Delete 删除数据。
func (m *MockDB) Delete(key any) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.data, key)
}
// Clear 清空数据。
func (m *MockDB) Clear() {
m.mu.Lock()
defer m.mu.Unlock()
m.data = make(map[any]any)
}
// MockCache 是并发安全的简单内存缓存 mock。
type MockCache struct {
mu sync.RWMutex
data map[string][]byte
}
// NewMockCache 创建模拟缓存。
func NewMockCache() *MockCache {
return &MockCache{data: make(map[string][]byte)}
}
// Set 设置缓存,内部会复制 value,避免调用方后续修改污染 mock。
func (m *MockCache) Set(key string, value []byte) {
m.mu.Lock()
defer m.mu.Unlock()
m.data[key] = append([]byte(nil), value...)
}
// Get 获取缓存,返回值会复制一份,避免调用方修改内部状态。
func (m *MockCache) Get(key string) ([]byte, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
v, ok := m.data[key]
return append([]byte(nil), v...), ok
}
// Delete 删除缓存。
func (m *MockCache) Delete(key string) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.data, key)
}
// Exists 检查缓存是否存在。
func (m *MockCache) Exists(key string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
_, ok := m.data[key]
return ok
}
// Clear 清空缓存。
func (m *MockCache) Clear() {
m.mu.Lock()
defer m.mu.Unlock()
m.data = make(map[string][]byte)
}
// ErrMockStorageTooLarge 表示 mock storage 收到超过内存保护上限的内容。
var ErrMockStorageTooLarge = errors.New("mock storage: 文件超过大小限制")
const mockStorageMaxBytes int64 = 32 << 20
// MockStorage 是并发安全的简单内存存储 mock。
type MockStorage struct {
mu sync.RWMutex
files map[string][]byte
urls map[string]string
}
// NewMockStorage 创建模拟存储。
func NewMockStorage() *MockStorage {
return &MockStorage{
files: make(map[string][]byte),
urls: make(map[string]string),
}
}
// Upload 模拟上传,签名对齐 storage.Upload。
func (m *MockStorage) Upload(file *multipart.FileHeader, subdir string) (string, error) {
if file == nil {
return "", errors.New("mock storage: 文件不能为空")
}
if file.Size > mockStorageMaxBytes {
return "", ErrMockStorageTooLarge
}
data, err := file.Open()
if err != nil {
return "", err
}
b, err := io.ReadAll(io.LimitReader(data, mockStorageMaxBytes+1))
if cerr := data.Close(); cerr != nil {
err = errors.Join(err, cerr)
}
if err != nil {
return "", err
}
if int64(len(b)) > mockStorageMaxBytes {
return "", ErrMockStorageTooLarge
}
path := "/mock/" + subdir + "/" + file.Filename
m.mu.Lock()
defer m.mu.Unlock()
m.files[path] = b
return path, nil
}
// UploadFromBytes 模拟字节上传,签名对齐 storage.UploadFromBytes。
func (m *MockStorage) UploadFromBytes(data []byte, filename, subdir string) (string, error) {
if int64(len(data)) > mockStorageMaxBytes {
return "", ErrMockStorageTooLarge
}
path := "/mock/" + subdir + "/" + filename
m.mu.Lock()
defer m.mu.Unlock()
m.files[path] = append([]byte(nil), data...)
return path, nil
}
// GetURL 获取 mock URL。
func (m *MockStorage) GetURL(path string) string {
return "http://mock.test" + path
}
// AssertEqual 断言相等。
func AssertEqual(t *testing.T, expected, actual any) {
t.Helper()
if expected != actual {
t.Errorf("不相等: 期望 %v, 实际 %v", expected, actual)
}
}
// AssertNotNil 断言非空。
func AssertNotNil(t *testing.T, value any) {
t.Helper()
if value == nil {
t.Error("期望非空,实际为空")
}
}
// AssertNil 断言为空。
func AssertNil(t *testing.T, value any) {
t.Helper()
if value != nil {
t.Errorf("期望为空,实际为: %v", value)
}
}
// AssertTrue 断言为 true。
func AssertTrue(t *testing.T, value bool) {
t.Helper()
if !value {
t.Error("期望为 true,实际为 false")
}
}
// AssertFalse 断言为 false。
func AssertFalse(t *testing.T, value bool) {
t.Helper()
if value {
t.Error("期望为 false,实际为 true")
}
}
// AssertError 断言有错误。
func AssertError(t *testing.T, err error) {
t.Helper()
if err == nil {
t.Error("期望有错误,实际无错误")
}
}
// AssertNoError 断言无错误。
func AssertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Errorf("期望无错误,实际错误: %v", err)
}
}