| name | writing-tests |
| description | Write unit tests and integration tests for Go code using testify, go-sqlmock, and mockery. Use when writing tests, creating test files, or testing repositories, use cases, and handlers. |
| allowed-tools | Read, Write, Edit, Bash, Glob |
Writing Tests
This skill guides you through writing tests for this Go project using testify, go-sqlmock, and mockery-generated mocks.
Test Commands
make test # Run all tests
make test-coverage # Generate HTML coverage report
make test-coverage-report # Show coverage in terminal
make mock-gen # Generate mocks from interfaces
# Run specific tests
go test ./internal/features/auth/...
go test -run TestCreateUser ./...
go test -v ./internal/shared/infrastructure/repository/
Test File Structure
Test files follow Go conventions:
- Named
*_test.go - In the same package as the code being tested
- Use
package packagename(notpackagename_test)
internal/features/auth/
├── usecase/
│ ├── auth_usecase.go
│ └── auth_usecase_test.go # Test file
└── delivery/http/handler/
├── auth_handler.go
└── auth_handler_test.go # Test file
Repository Tests (with go-sqlmock)
Repository tests use go-sqlmock to mock database interactions without a real database.
Example: Testing a Repository
package repository
import (
"app/internal/shared/domain/entity"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func setupMockDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock) {
// Create mock DB
mockDB, mock, err := sqlmock.New()
assert.NoError(t, err)
// Create GORM DB with mock
dialector := postgres.New(postgres.Config{
Conn: mockDB,
DriverName: "postgres",
})
db, err := gorm.Open(dialector, &gorm.Config{})
assert.NoError(t, err)
return db, mock
}
func TestUserRepository_Create(t *testing.T) {
db, mock := setupMockDB(t)
repo := NewUserRepository(db)
user := &entity.User{
ID: uuid.New(),
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
FullName: "Test User",
}
// Expect INSERT query
mock.ExpectBegin()
mock.ExpectExec(`INSERT INTO "users"`).
WithArgs(
user.ID,
user.Username,
user.Email,
user.Password,
user.FullName,
sqlmock.AnyArg(), // created_at
sqlmock.AnyArg(), // updated_at
).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
// Execute
err := repo.Create(user)
// Assert
assert.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestUserRepository_FindByEmail(t *testing.T) {
db, mock := setupMockDB(t)
repo := NewUserRepository(db)
expectedUser := &entity.User{
ID: uuid.New(),
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
FullName: "Test User",
}
// Define expected query result
rows := sqlmock.NewRows([]string{
"id", "username", "email", "password", "full_name", "created_at", "updated_at",
}).AddRow(
expectedUser.ID,
expectedUser.Username,
expectedUser.Email,
expectedUser.Password,
expectedUser.FullName,
time.Now(),
time.Now(),
)
// Expect SELECT query
mock.ExpectQuery(`SELECT \* FROM "users" WHERE email = \$1`).
WithArgs("test@example.com").
WillReturnRows(rows)
// Execute
result, err := repo.FindByEmail("test@example.com")
// Assert
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, expectedUser.Email, result.Email)
assert.Equal(t, expectedUser.Username, result.Username)
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestUserRepository_Update(t *testing.T) {
db, mock := setupMockDB(t)
repo := NewUserRepository(db)
user := &entity.User{
ID: uuid.New(),
Username: "testuser",
Email: "test@example.com",
FullName: "Updated Name",
}
// Expect UPDATE query
mock.ExpectBegin()
mock.ExpectExec(`UPDATE "users" SET`).
WithArgs(
user.Username,
user.Email,
user.FullName,
sqlmock.AnyArg(), // updated_at
user.ID,
).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
// Execute
err := repo.Update(user)
// Assert
assert.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestUserRepository_Delete(t *testing.T) {
db, mock := setupMockDB(t)
repo := NewUserRepository(db)
userID := uuid.New()
// Expect DELETE query
mock.ExpectBegin()
mock.ExpectExec(`DELETE FROM "users" WHERE "users"."id" = \$1`).
WithArgs(userID).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
// Execute
err := repo.Delete(userID)
// Assert
assert.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
Common sqlmock Patterns
Expect query with specific columns:
rows := sqlmock.NewRows([]string{"id", "name", "email"}).
AddRow(1, "John", "john@example.com").
AddRow(2, "Jane", "jane@example.com")
mock.ExpectQuery(`SELECT \* FROM users`).WillReturnRows(rows)
Expect error:
mock.ExpectQuery(`SELECT \* FROM users`).
WillReturnError(gorm.ErrRecordNotFound)
Transaction expectations:
mock.ExpectBegin()
mock.ExpectExec(`INSERT INTO...`).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
Use Case Tests (with Mocks)
Use case tests use mockery-generated mocks to test business logic without dependencies.
Step 1: Generate Mocks
make mock-gen
This generates mocks in internal/mocks/ based on .mockery.yaml.
Step 2: Write Use Case Tests
package usecase
import (
"app/internal/features/auth/delivery/http/dto"
"app/internal/shared/constants"
"app/internal/shared/domain/entity"
mocks "app/internal/mocks/repository"
"errors"
"testing"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestAuthUsecase_Register_Success(t *testing.T) {
// Setup
mockRepo := mocks.NewMockUserRepository(t)
logger := logrus.New()
usecase := NewAuthUsecase(mockRepo, logger)
req := &dto.RegisterRequest{
Username: "newuser",
Email: "new@example.com",
Password: "password123",
FullName: "New User",
}
// Mock expectations
mockRepo.EXPECT().
FindByEmail(req.Email).
Return(nil, gorm.ErrRecordNotFound).
Once()
mockRepo.EXPECT().
FindByUsername(req.Username).
Return(nil, gorm.ErrRecordNotFound).
Once()
mockRepo.EXPECT().
Create(mock.AnythingOfType("*entity.User")).
Return(nil).
Once()
// Execute
result, err := usecase.Register(req)
// Assert
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, req.Email, result.Email)
assert.Equal(t, req.Username, result.Username)
mockRepo.AssertExpectations(t)
}
func TestAuthUsecase_Register_EmailAlreadyExists(t *testing.T) {
// Setup
mockRepo := mocks.NewMockUserRepository(t)
logger := logrus.New()
usecase := NewAuthUsecase(mockRepo, logger)
req := &dto.RegisterRequest{
Username: "newuser",
Email: "existing@example.com",
Password: "password123",
}
existingUser := &entity.User{
ID: uuid.New(),
Email: req.Email,
}
// Mock expectations - email already exists
mockRepo.EXPECT().
FindByEmail(req.Email).
Return(existingUser, nil).
Once()
// Execute
result, err := usecase.Register(req)
// Assert
assert.Error(t, err)
assert.Nil(t, result)
assert.Equal(t, constants.GetError(constants.UserAlreadyExists, constants.LangEN), err)
mockRepo.AssertExpectations(t)
}
func TestAuthUsecase_Login_Success(t *testing.T) {
// Setup
mockRepo := mocks.NewMockUserRepository(t)
logger := logrus.New()
usecase := NewAuthUsecase(mockRepo, logger)
hashedPassword, _ := crypto.HashPassword("password123")
user := &entity.User{
ID: uuid.New(),
Email: "user@example.com",
Password: hashedPassword,
}
req := &dto.LoginRequest{
Email: "user@example.com",
Password: "password123",
}
// Mock expectations
mockRepo.EXPECT().
FindByEmail(req.Email).
Return(user, nil).
Once()
// Execute
result, err := usecase.Login(req)
// Assert
assert.NoError(t, err)
assert.NotNil(t, result)
assert.NotEmpty(t, result.Token)
mockRepo.AssertExpectations(t)
}
func TestAuthUsecase_Login_InvalidPassword(t *testing.T) {
// Setup
mockRepo := mocks.NewMockUserRepository(t)
logger := logrus.New()
usecase := NewAuthUsecase(mockRepo, logger)
hashedPassword, _ := crypto.HashPassword("correctpassword")
user := &entity.User{
ID: uuid.New(),
Email: "user@example.com",
Password: hashedPassword,
}
req := &dto.LoginRequest{
Email: "user@example.com",
Password: "wrongpassword",
}
// Mock expectations
mockRepo.EXPECT().
FindByEmail(req.Email).
Return(user, nil).
Once()
// Execute
result, err := usecase.Login(req)
// Assert
assert.Error(t, err)
assert.Nil(t, result)
mockRepo.AssertExpectations(t)
}
Mock Expectations Patterns
Basic expectation:
mockRepo.EXPECT().
MethodName(arg1, arg2).
Return(result, nil).
Once()
Any argument:
mockRepo.EXPECT().
Create(mock.AnythingOfType("*entity.User")).
Return(nil).
Once()
Multiple calls:
mockRepo.EXPECT().FindByID(userID).Return(user, nil).Times(3)
Different returns per call:
mockRepo.EXPECT().FindByID(userID).Return(user, nil).Once()
mockRepo.EXPECT().FindByID(userID).Return(nil, errors.New("error")).Once()
Argument matcher:
mockRepo.EXPECT().
Create(mock.MatchedBy(func(u *entity.User) bool {
return u.Email == "test@example.com"
})).
Return(nil).
Once()
Handler Tests (HTTP Tests)
Handler tests use httptest to test HTTP endpoints without a running server.
package handler
import (
"app/internal/features/auth/delivery/http/dto"
mocks "app/internal/mocks/usecase"
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func setupRouter() *gin.Engine {
gin.SetMode(gin.TestMode)
return gin.New()
}
func TestAuthHandler_Register_Success(t *testing.T) {
// Setup
mockUsecase := mocks.NewMockAuthUsecase(t)
handler := NewAuthHandler(mockUsecase)
router := setupRouter()
router.POST("/register", handler.Register)
req := dto.RegisterRequest{
Username: "testuser",
Email: "test@example.com",
Password: "password123",
FullName: "Test User",
}
expectedResponse := &dto.RegisterResponse{
ID: "123e4567-e89b-12d3-a456-426614174000",
Username: req.Username,
Email: req.Email,
}
// Mock expectations
mockUsecase.EXPECT().
Register(&req).
Return(expectedResponse, nil).
Once()
// Create request
body, _ := json.Marshal(req)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPost, "/register", bytes.NewBuffer(body))
r.Header.Set("Content-Type", "application/json")
// Execute
router.ServeHTTP(w, r)
// Assert
assert.Equal(t, http.StatusCreated, w.Code)
var response map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &response)
assert.False(t, response["error"].(bool))
assert.NotNil(t, response["data"])
mockUsecase.AssertExpectations(t)
}
func TestAuthHandler_Register_ValidationError(t *testing.T) {
// Setup
mockUsecase := mocks.NewMockAuthUsecase(t)
handler := NewAuthHandler(mockUsecase)
router := setupRouter()
router.POST("/register", handler.Register)
// Invalid request (missing required fields)
req := dto.RegisterRequest{
Username: "", // Empty username
Email: "invalid-email", // Invalid email
}
// Create request
body, _ := json.Marshal(req)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPost, "/register", bytes.NewBuffer(body))
r.Header.Set("Content-Type", "application/json")
// Execute
router.ServeHTTP(w, r)
// Assert
assert.Equal(t, http.StatusBadRequest, w.Code)
var response map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &response)
assert.True(t, response["error"].(bool))
assert.NotNil(t, response["errors"])
}
func TestAuthHandler_Login_Success(t *testing.T) {
// Setup
mockUsecase := mocks.NewMockAuthUsecase(t)
handler := NewAuthHandler(mockUsecase)
router := setupRouter()
router.POST("/login", handler.Login)
req := dto.LoginRequest{
Email: "test@example.com",
Password: "password123",
}
expectedResponse := &dto.LoginResponse{
Token: "jwt.token.here",
User: dto.UserInfo{
ID: "123e4567-e89b-12d3-a456-426614174000",
Email: req.Email,
Username: "testuser",
},
}
// Mock expectations
mockUsecase.EXPECT().
Login(&req).
Return(expectedResponse, nil).
Once()
// Create request
body, _ := json.Marshal(req)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPost, "/login", bytes.NewBuffer(body))
r.Header.Set("Content-Type", "application/json")
// Execute
router.ServeHTTP(w, r)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &response)
assert.False(t, response["error"].(bool))
mockUsecase.AssertExpectations(t)
}
Test Organization
Table-Driven Tests
For testing multiple scenarios:
func TestUserRepository_FindByEmail(t *testing.T) {
tests := []struct {
name string
email string
setupMock func(sqlmock.Sqlmock)
expectedError bool
}{
{
name: "user found",
email: "found@example.com",
setupMock: func(mock sqlmock.Sqlmock) {
rows := sqlmock.NewRows([]string{"id", "email"}).
AddRow(uuid.New(), "found@example.com")
mock.ExpectQuery(`SELECT`).WillReturnRows(rows)
},
expectedError: false,
},
{
name: "user not found",
email: "notfound@example.com",
setupMock: func(mock sqlmock.Sqlmock) {
mock.ExpectQuery(`SELECT`).WillReturnError(gorm.ErrRecordNotFound)
},
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db, mock := setupMockDB(t)
repo := NewUserRepository(db)
tt.setupMock(mock)
result, err := repo.FindByEmail(tt.email)
if tt.expectedError {
assert.Error(t, err)
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
}
})
}
}
Running and Analyzing Tests
Run tests with coverage
make test-coverage
View coverage in browser
make test-coverage
open coverage.html
Run specific package
go test -v ./internal/features/auth/usecase/
Run specific test
go test -v -run TestAuthUsecase_Register_Success ./internal/features/auth/usecase/
Run with race detector
go test -race ./...
Best Practices
- Test file naming: Use
*_test.gosuffix - Test function naming:
Test{FunctionName}_{Scenario} - Mock generation: Run
make mock-genafter changing interfaces - Coverage target: Aim for 80%+ coverage
- Arrange-Act-Assert: Structure tests clearly
- Test one thing: Each test should verify one behavior
- Use table-driven tests: For multiple similar scenarios
- Clean up mocks: Always call
AssertExpectations(t)