Skip to Content

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.go

Run all tests with:

go test ./...

Or use the Makefile shortcut:

make test

The testutil/testdb Package

github.com/gofastadev/gofasta/pkg/testutil/testdb exposes one primary helper:

func SetupTestDB(t *testing.T) *gorm.DB

It 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

Next Steps

Last updated on