+1

GOLANG INTEGRATION TEST VỚI GIN, GORM, TESTIFY, MYSQL

Trong bài này chúng ta sẽ viết một chương trình integration test cơ bản cho ứng dụng Golang sử dụng các thư viện như Gin, Gorm, Testify, và MySQL (sử dụng giải pháp in-memory) bao gồm việc setup môi trường testing, định nghĩa các routes và handlers, và test chúng với một real database (mặc dù việc dùng MySQL in-memory có thể cần đến giải pháp thay thế như sử dụng SQLite ở chế độ in-memory để cho đơn giản hơn).

Dưới đây là ví dụ về cách thiết lập một chương trình integration test:

1. Dependencies:

  • Gin: dùng để tạo HTTP server.
  • Gorm: dùng cho ORM để tương tác với cơ sở dữ liệu.
  • Testify: dùng để hỗ trợ kiểm tra (assertions).
  • SQLite in-memory: đóng vai trò thay thế cho MySQL trong quá trình kiểm thử.

2. Setup:

  • Định nghĩa một model cơ bản và thiết lập Gorm.
  • Tạo HTTP routes và handlers.
  • Viết các bài test sử dụng TestifySQLite như một in-memory database.

Dưới đây là ví dụ đầy đủ:

// main.go
package main

import (
    "github.com/gin-gonic/gin"
    "gorm.io/driver/mysql"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "net/http"
)

// User represents a simple user model.
type User struct {
    ID    uint   `gorm:"primaryKey"`
    Name  string `json:"name"`
    Email string `json:"email" gorm:"unique"`
}

// SetupRouter initializes the Gin engine with routes.
func SetupRouter(db *gorm.DB) *gin.Engine {
    r := gin.Default()

    // Inject the database into the handler
    r.POST("/users", func(c *gin.Context) {
        var user User
        if err := c.ShouldBindJSON(&user); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        if err := db.Create(&user).Error; err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }
        c.JSON(http.StatusCreated, user)
    })

    r.GET("/users/:id", func(c *gin.Context) {
        var user User
        id := c.Param("id")
        if err := db.First(&user, id).Error; err != nil {
            c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
            return
        }
        c.JSON(http.StatusOK, user)
    })
    
    r.PUT("/users/:id", func(c *gin.Context) {
        var user User
        id := c.Param("id")

        if err := db.First(&user, id).Error; err != nil {
            c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
            return
        }

        if err := c.ShouldBindJSON(&user); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        if err := db.Save(&user).Error; err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
    }
    
    r.DELETE("/users/:id", func(c *gin.Context) {
        id := c.Param("id")

        if err := db.Delete(&User{}, id).Error; err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }

        c.JSON(http.StatusOK, gin.H{"message": "User deleted"})
     })

    c.JSON(http.StatusOK, user)
})

    return r
}

func main() {
    // For production, use MySQL
    dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }

    db.AutoMigrate(&User{})

    r := SetupRouter(db)
    r.Run(":8080")
}

Integration Test

// main_test.go
package main

import (
    "bytes"
    "encoding/json"
    "github.com/stretchr/testify/assert"
    "net/http"
    "net/http/httptest"
    "testing"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

// SetupTestDB sets up an in-memory SQLite database for testing.
func SetupTestDB() *gorm.DB {
    db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    if err != nil {
        panic("failed to connect to the test database")
    }
    db.AutoMigrate(&User{})
    return db
}

func TestCreateUser(t *testing.T) {
    db := SetupTestDB()
    r := SetupRouter(db)

    // Create a new user.
    user := User{Name: "John Doe", Email: "john@example.com"}
    jsonValue, _ := json.Marshal(user)
    req, _ := http.NewRequest("POST", "/users", bytes.NewBuffer(jsonValue))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    assert.Equal(t, http.StatusCreated, w.Code)

    var createdUser User
    json.Unmarshal(w.Body.Bytes(), &createdUser)
    assert.Equal(t, "John Doe", createdUser.Name)
    assert.Equal(t, "john@example.com", createdUser.Email)
}

func TestGetUser(t *testing.T) {
    db := SetupTestDB()
    r := SetupRouter(db)

    // Insert a user into the in-memory database.
    user := User{Name: "Jane Doe", Email: "jane@example.com"}
    db.Create(&user)

    // Make a GET request.
    req, _ := http.NewRequest("GET", "/users/1", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    assert.Equal(t, http.StatusOK, w.Code)

    var fetchedUser User
    json.Unmarshal(w.Body.Bytes(), &fetchedUser)
    assert.Equal(t, "Jane Doe", fetchedUser.Name)
    assert.Equal(t, "jane@example.com", fetchedUser.Email)
}

func TestGetUserNotFound(t *testing.T) {
    db := SetupTestDB()
    r := SetupRouter(db)

    // Make a GET request for a non-existent user.
    req, _ := http.NewRequest("GET", "/users/999", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    assert.Equal(t, http.StatusNotFound, w.Code)
}

func TestUpdateUser(t *testing.T) {
    db := SetupTestDB()
    r := SetupRouter(db)

    // Insert a user into the in-memory database.
    user := User{Name: "Jane Doe", Email: "jane@example.com"}
    db.Create(&user)

    // Update user details.
    updatedUser := User{Name: "Jane Smith", Email: "jane.smith@example.com"}
    jsonValue, _ := json.Marshal(updatedUser)
    req, _ := http.NewRequest("PUT", "/users/1", bytes.NewBuffer(jsonValue))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    assert.Equal(t, http.StatusOK, w.Code)

    var fetchedUser User
    json.Unmarshal(w.Body.Bytes(), &fetchedUser)
    assert.Equal(t, "Jane Smith", fetchedUser.Name)
    assert.Equal(t, "jane.smith@example.com", fetchedUser.Email)
}

func TestDeleteUser(t *testing.T) {
    db := SetupTestDB()
    r := SetupRouter(db)

    // Insert a user into the in-memory database.
    user := User{Name: "Jane Doe", Email: "jane@example.com"}
    db.Create(&user)

    // Delete the user.
    req, _ := http.NewRequest("DELETE", "/users/1", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    assert.Equal(t, http.StatusOK, w.Code)

    // Verify that the user is deleted.
    var fetchedUser User
    err := db.First(&fetchedUser, user.ID).Error
    assert.Error(t, err)
    assert.Equal(t, gorm.ErrRecordNotFound, err)
}

Giải thích

  1. main.go:
  • Định nghĩa một User struct và thiết lập các thao tác CRUD cơ bản sử dụng Gin.
  • Sử dụng Gorm để tương tác với cơ sở dữ liệu và tự động migrate bảng User.
  • SetupRouter cấu hình các HTTP endpoint.
  1. main_test.go:
  • SetupTestDB thiết lập SQLite in-memory database cho việc testing.
  • TestCreateUser: Test việc tạo một user.
  • TestGetUser: Test việc lấy một user đã tồn tại.
  • TestGetUserNotFound: Test việc lấy một user không tồn tại.
  • TestUpdateUser: Test việc update một user đã tồn tại.
  • TestDeleteUser: Test việc delete một user đã tồn tại.
  • Sử dụng httptest.NewRecorderhttp.NewRequest để giả lập các request và response HTTP.
  • Sử dụng Testify để kiểm tra, như test HTTP status code và xác minh phản hồi JSON.

Khởi chạy chương trình test

Để run test, sử dụng lệnh:

go test -v

Lưu ý

  • SQLite for In-memory Testing: Ví dụ này sử dụng SQLite cho việc kiểm thử in-memoryMySQL không hỗ trợ chế độ in-memory với Gorm. Đối với các chương trình test cần phụ thuộc vào các tính năng cụ thể của MySQL, cân nhắc sử dụng giải pháp dựa trên Docker với một container MySQL.
  • Database Migrations: Luôn đảm bảo schema của cơ sở dữ liệu được cập nhật bằng cách sử dụng AutoMigrate trong các chương trình test.
  • Isolation: Mỗi hàm test sẽ khởi tạo một in-memory database mới, đảm bảo các hàm test không gây ảnh hưởng lẫn nhau.

Nếu bạn thấy bài viết này hữu ích, hãy cho mình biết bằng cách để lại 👍 hoặc bình luận!, hoặc nếu bạn nghĩ bài viết này có thể giúp ích cho ai đó, hãy thoải mái chia sẻ! Cảm ơn bạn rất nhiều! 😃


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí