Skip to main content

Gin Mock Database

Introduction

When testing Gin applications, one common challenge is managing database interactions. In a test environment, you ideally want to:

  • Test your API handlers without hitting a real database
  • Control the database responses to test different scenarios
  • Ensure tests run quickly and independently
  • Avoid test pollution and side effects

This is where mock databases come in. Mock databases simulate the behavior of a real database, allowing you to write predictable tests for your Gin handlers without depending on actual database connections.

In this guide, we'll explore how to create and use mock databases when testing your Gin applications.

Why Use Mock Databases?

Before diving into implementation, let's understand why mocking your database is beneficial:

  1. Speed: Tests run faster without real database operations
  2. Isolation: Tests don't depend on database state
  3. Predictability: You control exactly what data is returned
  4. Coverage: You can simulate error conditions that are hard to trigger with real databases
  5. No setup: No need to configure test databases or clean up data

Setting Up Mock Databases in Gin

Step 1: Define Your Database Interface

A good practice for testable code is programming against interfaces rather than concrete implementations:

go
// database.go
package database

type UserRepository interface {
GetUserByID(id string) (*User, error)
CreateUser(user *User) error
UpdateUser(user *User) error
DeleteUser(id string) error
}

type User struct {
ID string
Username string
Email string
}

Step 2: Create a Real Implementation

Your actual implementation will connect to a real database:

go
// postgres_repository.go
package database

import (
"database/sql"
_ "github.com/lib/pq"
)

type PostgresUserRepository struct {
db *sql.DB
}

func NewPostgresUserRepository(connectionString string) (*PostgresUserRepository, error) {
db, err := sql.Open("postgres", connectionString)
if err != nil {
return nil, err
}

return &PostgresUserRepository{db: db}, nil
}

func (r *PostgresUserRepository) GetUserByID(id string) (*User, error) {
// Real implementation with SQL queries
var user User
err := r.db.QueryRow("SELECT id, username, email FROM users WHERE id = $1", id).Scan(
&user.ID, &user.Username, &user.Email)
if err != nil {
return nil, err
}
return &user, nil
}

// Other methods implementation...

Step 3: Create a Mock Implementation for Testing

Now you can create a mock implementation of your interface for testing:

go
// mock_repository.go
package database

import "errors"

// MockUserRepository provides a way to mock database operations
type MockUserRepository struct {
users map[string]*User
// Error flags that can be set to test error conditions
ShouldFailGetUser bool
ShouldFailCreateUser bool
}

// NewMockUserRepository creates a new mock repository with optional initial data
func NewMockUserRepository(initialUsers ...*User) *MockUserRepository {
repo := &MockUserRepository{
users: make(map[string]*User),
}

for _, user := range initialUsers {
repo.users[user.ID] = user
}

return repo
}

// GetUserByID returns a user by ID or error if configured to fail
func (m *MockUserRepository) GetUserByID(id string) (*User, error) {
if m.ShouldFailGetUser {
return nil, errors.New("simulated database error")
}

user, exists := m.users[id]
if !exists {
return nil, errors.New("user not found")
}

return user, nil
}

// Other methods implementation with similar error flags...

Using Mock Databases in Gin Handler Tests

Now we can use our mock in our Gin handler tests. Let's first define a simple handler:

go
// handlers.go
package handlers

import (
"net/http"
"your-project/database"

"github.com/gin-gonic/gin"
)

type UserHandler struct {
repo database.UserRepository
}

func NewUserHandler(repo database.UserRepository) *UserHandler {
return &UserHandler{repo: repo}
}

func (h *UserHandler) GetUser(c *gin.Context) {
id := c.Param("id")

user, err := h.repo.GetUserByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}

c.JSON(http.StatusOK, user)
}

Testing the Handler with Mock Database

Now we can test our handler using the mock:

go
// handlers_test.go
package handlers

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"your-project/database"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)

func TestGetUser(t *testing.T) {
gin.SetMode(gin.TestMode)

t.Run("Success", func(t *testing.T) {
// Setup mock with sample data
mockRepo := database.NewMockUserRepository(
&database.User{ID: "123", Username: "testuser", Email: "[email protected]"},
)

// Create handler with mock repo
handler := NewUserHandler(mockRepo)

// Setup router and request
router := gin.New()
router.GET("/users/:id", handler.GetUser)

// Create test request
req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
resp := httptest.NewRecorder()

// Perform request
router.ServeHTTP(resp, req)

// Check response
assert.Equal(t, http.StatusOK, resp.Code)

var respUser database.User
err := json.Unmarshal(resp.Body.Bytes(), &respUser)
assert.NoError(t, err)
assert.Equal(t, "123", respUser.ID)
assert.Equal(t, "testuser", respUser.Username)
assert.Equal(t, "[email protected]", respUser.Email)
})

t.Run("User Not Found", func(t *testing.T) {
// Empty mock repository with no users
mockRepo := database.NewMockUserRepository()

handler := NewUserHandler(mockRepo)

router := gin.New()
router.GET("/users/:id", handler.GetUser)

req := httptest.NewRequest(http.MethodGet, "/users/999", nil)
resp := httptest.NewRecorder()

router.ServeHTTP(resp, req)

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

t.Run("Database Error", func(t *testing.T) {
mockRepo := database.NewMockUserRepository(
&database.User{ID: "123", Username: "testuser", Email: "[email protected]"},
)

// Configure mock to simulate database error
mockRepo.ShouldFailGetUser = true

handler := NewUserHandler(mockRepo)

router := gin.New()
router.GET("/users/:id", handler.GetUser)

req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
resp := httptest.NewRecorder()

router.ServeHTTP(resp, req)

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

Advanced Mock Database Techniques

Using Testify Mock

For more complex testing scenarios, you might want to use a mocking library like Testify Mock. This gives you more control over your mock's behavior:

go
// testify_mock_example_test.go
package database_test

import (
"testing"
"your-project/database"

"github.com/stretchr/testify/mock"
)

// Create a mock that satisfies your interface
type MockUserRepository struct {
mock.Mock
}

func (m *MockUserRepository) GetUserByID(id string) (*database.User, error) {
// The mock will use the arguments passed to the method to look up
// pre-defined return values
args := m.Called(id)

// If there's a User in the return value, return it
if user, ok := args.Get(0).(*database.User); ok {
return user, args.Error(1)
}

return nil, args.Error(1)
}

// Other methods implementation...

func TestWithTestifyMock(t *testing.T) {
mockRepo := new(MockUserRepository)

// Define behavior: when GetUserByID is called with "123", return this user and nil error
mockRepo.On("GetUserByID", "123").Return(
&database.User{ID: "123", Username: "mocked", Email: "[email protected]"},
nil,
)

// Define behavior for a different ID
mockRepo.On("GetUserByID", "456").Return(nil, errors.New("user not found"))

// Now you can use mockRepo in your tests
// ...

// Verify all expectations were met
mockRepo.AssertExpectations(t)
}

Using gomock

Another popular option is Google's gomock:

First, install the mockgen tool:

bash
go install github.com/golang/mock/mockgen@latest

Generate the mock for your interface:

bash
mockgen -source=database/database.go -destination=mocks/mock_database.go -package=mocks

Then use it in your tests:

go
// gomock_example_test.go
package handlers_test

import (
"testing"
"errors"
"your-project/database"
"your-project/handlers"
"your-project/mocks"

"github.com/golang/mock/gomock"
)

func TestUserHandlerWithGoMock(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

// Create mock
mockRepo := mocks.NewMockUserRepository(ctrl)

// Set expectations
mockRepo.EXPECT().
GetUserByID("123").
Return(&database.User{ID: "123", Username: "gomock", Email: "[email protected]"}, nil)

mockRepo.EXPECT().
GetUserByID("456").
Return(nil, errors.New("not found"))

// Use mock in handler tests
// ...
}

Real-world Example: Testing CRUD API

Let's put everything together with a more complete example of a CRUD API:

go
// user_service.go
package services

import "your-project/database"

type UserService struct {
repo database.UserRepository
}

func NewUserService(repo database.UserRepository) *UserService {
return &UserService{repo: repo}
}

func (s *UserService) GetUser(id string) (*database.User, error) {
return s.repo.GetUserByID(id)
}

func (s *UserService) CreateUser(username, email string) (*database.User, error) {
// In a real app, you'd generate a unique ID
newUser := &database.User{
ID: "user_" + username, // simplified for example
Username: username,
Email: email,
}

err := s.repo.CreateUser(newUser)
if err != nil {
return nil, err
}

return newUser, nil
}

// Other service methods...
go
// user_handler.go
package handlers

import (
"net/http"
"your-project/services"

"github.com/gin-gonic/gin"
)

type UserHandler struct {
service *services.UserService
}

func NewUserHandler(service *services.UserService) *UserHandler {
return &UserHandler{service: service}
}

func (h *UserHandler) GetUser(c *gin.Context) {
id := c.Param("id")

user, err := h.service.GetUser(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}

c.JSON(http.StatusOK, user)
}

type CreateUserRequest struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required,email"`
}

func (h *UserHandler) CreateUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

user, err := h.service.CreateUser(req.Username, req.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
return
}

c.JSON(http.StatusCreated, user)
}

// Other handler methods...

Now let's test the handlers with a mock database:

go
// user_handler_test.go
package handlers_test

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"your-project/database"
"your-project/handlers"
"your-project/services"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)

func TestUserHandler(t *testing.T) {
gin.SetMode(gin.TestMode)

t.Run("CreateUser Success", func(t *testing.T) {
mockRepo := database.NewMockUserRepository()
service := services.NewUserService(mockRepo)
handler := handlers.NewUserHandler(service)

// Setup router and request
router := gin.New()
router.POST("/users", handler.CreateUser)

// Prepare request payload
payload := map[string]string{
"username": "newuser",
"email": "[email protected]",
}
jsonData, _ := json.Marshal(payload)

req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()

// Perform request
router.ServeHTTP(resp, req)

// Check response
assert.Equal(t, http.StatusCreated, resp.Code)

var respUser database.User
err := json.Unmarshal(resp.Body.Bytes(), &respUser)
assert.NoError(t, err)
assert.Equal(t, "user_newuser", respUser.ID) // Based on our simplified ID generation
assert.Equal(t, "newuser", respUser.Username)
assert.Equal(t, "[email protected]", respUser.Email)

// Verify the user was stored in our mock repository
storedUser, err := mockRepo.GetUserByID("user_newuser")
assert.NoError(t, err)
assert.NotNil(t, storedUser)
})

t.Run("CreateUser Invalid Input", func(t *testing.T) {
mockRepo := database.NewMockUserRepository()
service := services.NewUserService(mockRepo)
handler := handlers.NewUserHandler(service)

router := gin.New()
router.POST("/users", handler.CreateUser)

// Missing required fields
payload := map[string]string{
"username": "newuser",
// missing email
}
jsonData, _ := json.Marshal(payload)

req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()

router.ServeHTTP(resp, req)

assert.Equal(t, http.StatusBadRequest, resp.Code)
})

t.Run("CreateUser Database Error", func(t *testing.T) {
mockRepo := database.NewMockUserRepository()
mockRepo.ShouldFailCreateUser = true

service := services.NewUserService(mockRepo)
handler := handlers.NewUserHandler(service)

router := gin.New()
router.POST("/users", handler.CreateUser)

payload := map[string]string{
"username": "newuser",
"email": "[email protected]",
}
jsonData, _ := json.Marshal(payload)

req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()

router.ServeHTTP(resp, req)

assert.Equal(t, http.StatusInternalServerError, resp.Code)
})
}

Best Practices for Mock Databases

  1. Define clear interfaces: Always program against interfaces, not concrete implementations.

  2. Keep mocks simple: Start with simple mocks and add complexity as needed.

  3. Test edge cases: Use mocks to test error conditions and edge cases that are hard to reproduce with real databases.

  4. Avoid over-mocking: Don't mock everything; focus on the database interactions.

  5. Verify behavior: For complex tests, verify the mock was called with expected parameters.

  6. Consider integration tests: Mock databases are great for unit tests, but also include some integration tests with real databases.

  7. Match real behavior: Ensure your mocks behave similarly to real databases. For example, if your database returns specific errors, make your mock return the same error types.

  8. Keep mock implementations separate: Put your mocks in separate files or packages from your real implementations.

Summary

Mock databases are a powerful tool for testing Gin applications. They allow you to:

  • Test handlers without real database dependencies
  • Control response behavior for testing different scenarios
  • Write faster, more reliable tests
  • Test error conditions easily

In this guide, we've covered:

  • Why mock databases are useful
  • How to create simple mock implementations
  • Using mocking libraries like Testify Mock and gomock
  • Testing Gin handlers with mock databases
  • Best practices for effective mocking

By incorporating mock databases into your testing strategy, you can write more comprehensive tests for your Gin applications and catch issues before they reach production.

Additional Resources

Exercises

  1. Create a mock database for a product inventory API that supports CRUD operations.
  2. Extend the mock database from this guide to include filtering users by username.
  3. Write tests for an authentication handler that depends on database lookups.
  4. Implement transaction support in your mock database (begin, commit, rollback).
  5. Compare the performance of tests using a mock database versus a real database.

Happy testing!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)