Truong Phung
Posted on October 18, 2024
Creating a comprehensive integration test for a Golang application using libraries like Gin
, Gorm
, Testify
, and MySQL
(using an in-memory solution) involves setting up a testing environment, defining routes and handlers, and testing them against an actual database (though using MySQL in-memory might require a workaround like using SQLite in in-memory mode for simplicity).
Here’s an example of an integration test setup:
1. Dependencies:
-
Gin
: for creating the HTTP server. -
Gorm
: for ORM to interact with the database. -
Testify
: for assertions. -
SQLite
in-memory: acts as a substitute for MySQL during testing.
2. Setup:
- Define a basic model and Gorm setup.
- Create HTTP routes and handlers.
- Write tests using Testify and SQLite as an in-memory database.
Here’s the full example:
// 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)
}
Explanation
-
main.go
:- Defines a
User
struct and sets up basic CRUD operations usingGin
. - Uses Gorm for database interactions and auto-migrates the
User
table. -
SetupRouter
configures HTTP endpoints.
- Defines a
-
main_test.go
:-
SetupTestDB
initializes an in-memory SQLite database for isolated testing. -
TestCreateUser
: Tests the creation of a user. -
TestGetUser
: Tests fetching an existing user. -
TestGetUserNotFound
: Tests fetching a non-existent user. -
TestUpdateUser
: Tests update an existing user. -
TestDeleteUser
: Tests delete an existing user. - Uses
httptest.NewRecorder
andhttp.NewRequest
for simulating HTTP requests and responses. - Uses
Testify
for assertions, like checking HTTP status codes and verifying JSON responses.
-
Running the Tests
To run the tests, use:
go test -v
Considerations
-
SQLite for In-memory Testing: This example uses SQLite for in-memory testing as MySQL doesn't natively support an in-memory mode with
Gorm
. For tests that rely on MySQL-specific features, consider using a Docker-based setup with a MySQL container. -
Database Migrations: Always ensure the database schema is up-to-date using
AutoMigrate
in tests. - Isolation: Each test function initializes a fresh in-memory database, ensuring tests don't interfere with each other.
If you found this helpful, let me know by leaving a 👍 or a comment!, or if you think this post could help someone, feel free to share it! Thank you very much! 😃
Posted on October 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.