Testing
Gofasta projects use Go’s built-in testing package alongside the github.com/gofastadev/gofasta/pkg/testutil/testdb helper, which spins up a real PostgreSQL container via testcontainers-go for integration tests. For unit tests, mock your repository interfaces with hand-written stubs — Go interfaces make this trivial and no mocking library is required.
Project Test Structure
Tests follow Go conventions and live alongside the code they test:
app/
├── services/
│ ├── product.service.go
│ └── product.service_test.go
├── rest/controllers/
│ ├── product.controller.go
│ └── product.controller_test.go
├── repositories/
│ ├── product.repository.go
│ └── product.repository_test.goRun all tests with:
go test ./...Or use the Makefile shortcut:
make testThe testutil/testdb Package
github.com/gofastadev/gofasta/pkg/testutil/testdb exposes one primary helper:
func SetupTestDB(t *testing.T) *gorm.DBIt starts a fresh PostgreSQL container, runs the gofasta library’s base migrations (the citext extension and the shared trigger functions), and returns a ready-to-use *gorm.DB. The container is automatically torn down at the end of the test via t.Cleanup, so every test gets an isolated database.
Requirements: Docker must be running on the machine executing the tests. The first call pulls the postgres:16-alpine image.
Basic Integration Test
package repositories_test
import (
"context"
"testing"
"github.com/gofastadev/gofasta/pkg/testutil/testdb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"myapp/app/models"
"myapp/app/repositories"
)
func TestProductRepository_Create(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test (requires Docker)")
}
db := testdb.SetupTestDB(t)
require.NoError(t, db.AutoMigrate(&models.Product{}))
repo := repositories.NewProductRepository(db)
product := &models.Product{Name: "Widget", Price: 9.99}
err := repo.Create(context.Background(), product)
require.NoError(t, err)
assert.NotEmpty(t, product.ID) // BaseModelImpl auto-populates the UUID
}Splitting Unit and Integration Tests
Use the testing.Short() flag to skip container-backed tests when you want a fast unit-only run:
# Fast unit-only run — no Docker required
go test -short ./...
# Full run including container-backed integration tests
go test ./...Unit Testing Services
Services contain business logic and are the most important layer to test. They should be tested without hitting the database — mock the repository interface and exercise the service’s logic in isolation.
Hand-Written Repository Mocks
Gofasta generates interface-based repositories, so you can mock them with a simple struct holding function fields. No mock-generation tool is required.
// app/repositories/mocks/product_repository_mock.go
package mocks
import (
"context"
"myapp/app/models"
)
type MockProductRepository struct {
CreateFn func(ctx context.Context, product *models.Product) error
FindAllFn func(ctx context.Context, page, perPage int) ([]models.Product, int64, error)
FindByIDFn func(ctx context.Context, id string) (*models.Product, error)
UpdateFn func(ctx context.Context, product *models.Product) error
DeleteFn func(ctx context.Context, id string) error
}
func (m *MockProductRepository) Create(ctx context.Context, p *models.Product) error {
return m.CreateFn(ctx, p)
}
func (m *MockProductRepository) FindAll(ctx context.Context, page, perPage int) ([]models.Product, int64, error) {
return m.FindAllFn(ctx, page, perPage)
}
func (m *MockProductRepository) FindByID(ctx context.Context, id string) (*models.Product, error) {
return m.FindByIDFn(ctx, id)
}
func (m *MockProductRepository) Update(ctx context.Context, p *models.Product) error {
return m.UpdateFn(ctx, p)
}
func (m *MockProductRepository) Delete(ctx context.Context, id string) error {
return m.DeleteFn(ctx, id)
}Writing Service Tests
package services_test
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"myapp/app/dtos"
"myapp/app/models"
"myapp/app/repositories/mocks"
"myapp/app/services"
)
func TestProductService_Create(t *testing.T) {
mockRepo := &mocks.MockProductRepository{
CreateFn: func(ctx context.Context, p *models.Product) error {
p.ID = uuid.New()
return nil
},
}
svc := services.NewProductService(mockRepo)
result, err := svc.Create(context.Background(), &dtos.CreateProductRequest{
Name: "Widget",
Price: 9.99,
})
require.NoError(t, err)
assert.Equal(t, "Widget", result.Name)
assert.InDelta(t, 9.99, result.Price, 0.0001)
}
func TestProductService_Create_InvalidPrice(t *testing.T) {
svc := services.NewProductService(&mocks.MockProductRepository{})
_, err := svc.Create(context.Background(), &dtos.CreateProductRequest{
Name: "Widget",
Price: -5.00,
})
assert.Error(t, err)
}Because the mock repository is a plain struct, each test only has to set the function fields it actually cares about. Other methods will panic on a nil call — which is exactly the signal you want if a test exercises code it shouldn’t.
Testing Controllers
Controllers are standard net/http handlers, so test them with the standard library’s net/http/httptest. There is no special testing router; use the real chi.Router and httputil.Handle wrapper.
package controllers_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/gofastadev/gofasta/pkg/httputil"
"myapp/app/models"
"myapp/app/repositories/mocks"
"myapp/app/rest/controllers"
"myapp/app/services"
)
func TestProductController_Create(t *testing.T) {
mockRepo := &mocks.MockProductRepository{
CreateFn: func(ctx context.Context, p *models.Product) error {
p.ID = uuid.New()
return nil
},
}
svc := services.NewProductService(mockRepo)
ctrl := controllers.NewProductController(svc)
router := chi.NewRouter()
router.Post("/products", httputil.Handle(ctrl.Create))
body, _ := json.Marshal(map[string]any{"name": "Widget", "price": 9.99})
req := httptest.NewRequest(http.MethodPost, "/products", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusCreated, rec.Code)
assert.Contains(t, rec.Body.String(), `"name":"Widget"`)
}Testing Repositories
Repository tests use a real PostgreSQL database via testdb.SetupTestDB — GORM’s query behavior differs subtly between drivers, so testing against Postgres directly catches dialect issues early. Guard these tests with testing.Short() so they can be skipped on a fast local run:
package repositories_test
import (
"context"
"testing"
"github.com/gofastadev/gofasta/pkg/testutil/testdb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"myapp/app/models"
"myapp/app/repositories"
)
func TestProductRepository_FindByID(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test (requires Docker)")
}
db := testdb.SetupTestDB(t)
require.NoError(t, db.AutoMigrate(&models.Product{}))
repo := repositories.NewProductRepository(db)
// Seed
seed := &models.Product{Name: "Widget", Price: 9.99}
require.NoError(t, db.Create(seed).Error)
// Exercise
found, err := repo.FindByID(context.Background(), seed.ID.String())
require.NoError(t, err)
assert.Equal(t, "Widget", found.Name)
}Testing Protected Endpoints
Generate a JWT for the test user using the real pkg/auth package and attach it to the request as a Bearer token.
import "github.com/gofastadev/gofasta/pkg/auth"
func TestProductController_Create_Authenticated(t *testing.T) {
jwtSvc := auth.NewJWTService(&config.AuthConfig{
JWTSecret: "test-secret-at-least-32-bytes!!",
AccessTokenExpiry: time.Hour,
})
token, err := jwtSvc.GenerateToken("user-123", "admin")
require.NoError(t, err)
// ...build router + request as usual...
req.Header.Set("Authorization", "Bearer "+token)
}For an unauthenticated request, simply omit the header and assert you get 401 Unauthorized.
Running Tests
# Run all tests (includes container-backed integration tests — requires Docker)
go test ./...
# Fast unit-only run (skips anything guarded by testing.Short())
go test -short ./...
# Verbose output
go test -v ./...
# A specific package
go test ./app/services/...
# A specific test function
go test -run TestProductService_Create ./app/services/...
# Coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out