集成测试

集成测试验证多个组件协同工作的正确性,通常涉及数据库、外部服务等。

测试数据库

使用测试数据库进行集成测试:

package main

import (
    "database/sql"
    "net/http"
    "net/http/httptest"
    "testing"
    
    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
    _ "github.com/go-sql-driver/mysql"
)

var testDB *sql.DB

func TestMain(m *testing.M) {
    var err error
    testDB, err = sql.Open("mysql", "root:@tcp(localhost:3306)/test_db")
    if err != nil {
        panic(err)
    }
    defer testDB.Close()
    
    os.Exit(m.Run())
}

func setupTestDB(t *testing.T) {
    testDB.Exec("TRUNCATE TABLE users")
}

func TestGetUsers(t *testing.T) {
    gin.SetMode(gin.TestMode)
    setupTestDB(t)
    
    testDB.Exec("INSERT INTO users (name, email) VALUES (?, ?)", "张三", "test@example.com")
    
    r := gin.New()
    r.GET("/users", func(c *gin.Context) {
        rows, _ := testDB.Query("SELECT id, name, email FROM users")
        defer rows.Close()
        
        var users []gin.H
        for rows.Next() {
            var id int
            var name, email string
            rows.Scan(&id, &name, &email)
            users = append(users, gin.H{
                "id":    id,
                "name":  name,
                "email": email,
            })
        }
        
        c.JSON(http.StatusOK, users)
    })
    
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/users", nil)
    r.ServeHTTP(w, req)
    
    assert.Equal(t, http.StatusOK, w.Code)
}

使用事务回滚

每个测试用事务包裹,测试后回滚:

func withTransaction(t *testing.T, fn func(tx *sql.Tx)) {
    tx, err := testDB.Begin()
    if err != nil {
        t.Fatal(err)
    }
    
    defer tx.Rollback()
    
    fn(tx)
}

func TestCreateUser(t *testing.T) {
    withTransaction(t, func(tx *sql.Tx) {
        result, err := tx.Exec(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            "张三", "test@example.com",
        )
        assert.NoError(t, err)
        
        id, _ := result.LastInsertId()
        assert.Greater(t, id, int64(0))
    })
}

测试服务器

创建测试服务器:

func setupTestServer() *gin.Engine {
    r := gin.New()
    
    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{"status": "ok"})
    })
    
    api := r.Group("/api")
    api.Use(AuthMiddleware())
    {
        api.GET("/profile", getProfile)
        api.PUT("/profile", updateProfile)
    }
    
    return r
}

func TestHealthEndpoint(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    r := setupTestServer()
    
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/health", nil)
    r.ServeHTTP(w, req)
    
    assert.Equal(t, http.StatusOK, w.Code)
}

Mock 外部服务

使用 mock 替代外部依赖:

type UserService interface {
    GetUser(id string) (*User, error)
}

type MockUserService struct {
    users map[string]*User
}

func (m *MockUserService) GetUser(id string) (*User, error) {
    if user, ok := m.users[id]; ok {
        return user, nil
    }
    return nil, errors.New("user not found")
}

func TestGetUserWithMock(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    mockService := &MockUserService{
        users: map[string]*User{
            "1": {ID: "1", Name: "张三"},
        },
    }
    
    r := gin.New()
    r.GET("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        user, err := mockService.GetUser(id)
        if err != nil {
            c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
            return
        }
        c.JSON(http.StatusOK, user)
    })
    
    t.Run("用户存在", func(t *testing.T) {
        w := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", "/users/1", nil)
        r.ServeHTTP(w, req)
        
        assert.Equal(t, http.StatusOK, w.Code)
    })
    
    t.Run("用户不存在", func(t *testing.T) {
        w := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", "/users/999", nil)
        r.ServeHTTP(w, req)
        
        assert.Equal(t, http.StatusNotFound, w.Code)
    })
}

测试认证

func TestAuthenticatedEndpoint(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    r := gin.New()
    r.Use(AuthMiddleware())
    r.GET("/protected", func(c *gin.Context) {
        c.String(http.StatusOK, "ok")
    })
    
    t.Run("有效token", func(t *testing.T) {
        token := generateTestToken()
        
        w := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", "/protected", nil)
        req.Header.Set("Authorization", "Bearer "+token)
        r.ServeHTTP(w, req)
        
        assert.Equal(t, http.StatusOK, w.Code)
    })
    
    t.Run("无效token", func(t *testing.T) {
        w := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", "/protected", nil)
        req.Header.Set("Authorization", "Bearer invalid")
        r.ServeHTTP(w, req)
        
        assert.Equal(t, http.StatusUnauthorized, w.Code)
    })
}

func generateTestToken() string {
    return "test-valid-token"
}

测试文件上传

func TestFileUpload(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    r := gin.New()
    r.POST("/upload", func(c *gin.Context) {
        file, err := c.FormFile("file")
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        
        c.JSON(http.StatusOK, gin.H{
            "filename": file.Filename,
            "size":     file.Size,
        })
    })
    
    body := &bytes.Buffer{}
    writer := multipart.NewWriter(body)
    
    part, _ := writer.CreateFormFile("file", "test.txt")
    part.Write([]byte("test content"))
    writer.Close()
    
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("POST", "/upload", body)
    req.Header.Set("Content-Type", writer.FormDataContentType())
    r.ServeHTTP(w, req)
    
    assert.Equal(t, http.StatusOK, w.Code)
}

小结

集成测试验证组件间的协作。使用测试数据库或 mock 隔离外部依赖。事务回滚可以保持测试数据的干净。测试认证、文件上传等场景需要特殊处理请求构造。