Skip to content

Testing Patterns Guide ​

Comprehensive guide for writing tests in Promenade Platform (DDD Architecture)


Table of Contents ​

  1. Testing Philosophy
  2. Three-Tier Testing Strategy
  3. Integration Test Pattern
  4. Smoke Test Pattern
  5. Unit Test Pattern
  6. Best Practices
  7. Common Pitfalls
  8. Test Coverage Requirements

Testing Philosophy ​

Promenade follows strict DDD principles with three-tier testing strategy:

  1. Unit Tests - Fast feedback, test business logic in isolation
  2. Smoke Tests - Mock-based handler validation, NO database
  3. Integration Tests - Full E2E with real database

Key Principle: Each test type has ONE standardized pattern - максимальна ідентичність патерну для легкого розуміння та підтримки.


Three-Tier Testing Strategy ​

Overview ​

Unit Tests (in-place)          → Fast (5s)   → Business logic
Smoke Tests (mock-based)       → Fast (0.4s) → Handler HTTP contracts
Integration Tests (real DB)    → Slow (6s)   → Repository E2E

When to Use Each Type ​

Test TypeWhen to UseWhat to TestDependencies
UnitBusiness logic, entities, value objectsDomain rules, validationNone (mocks)
SmokeHTTP handlers, API contractsRequest/response, status codesMock UseCase
IntegrationRepository, database queriesSQL operations, transactionsReal PostgreSQL

Integration Test Pattern ​

Directory Structure ​

Mirror path structure - integration tests mirror production code:

internal/contexts/identity/user/
 entity.go
 usecase.go
 adapter/repository/postgres/
     user_repository.go

test/integration/contexts/identity/user/
 repository_test.go             ← mirrors adapter/repository/postgres/

Standardized Pattern ​

go
package user_test

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/basilex/promenade/internal/contexts/identity/user"
	"github.com/basilex/promenade/internal/contexts/identity/user/adapter/repository/postgres"
	"github.com/basilex/promenade/pkg/uuidv7"
	"github.com/basilex/promenade/test/integration"
)

//  ЕТАЛОННИЙ ПАТЕРН для інтеграційних тестів:

func TestUserRepository_Create(t *testing.T) {
	// 1. SetupTestDBWithCleanTables - TRUNCATE всіх таблиць (чиста DB для кожного test function)
	db := integration.SetupTestDBWithCleanTables(t)
	repo := postgres.NewUserRepository(db.DB)
	ctx := context.Background()

	// 2. Subtests - використовувати UUID тільки якщо потрібна uniqueness в межах test function
	t.Run("create user successfully", func(t *testing.T) {
		// Static email OK - SetupTestDBWithCleanTables дає чисту DB
		u, err := user.NewUser("test@example.com", "password123")
		require.NoError(t, err)

		err = repo.Create(ctx, u)
		require.NoError(t, err)
		assert.NotEqual(t, uuidv7.Nil, u.ID)
	})

	t.Run("duplicate email fails", func(t *testing.T) {
		// Reuse same email - DB was truncated, no conflict
		u1, _ := user.NewUser("test@example.com", "password123")
		u2, _ := user.NewUser("test@example.com", "password456")

		require.NoError(t, repo.Create(ctx, u1))
		err := repo.Create(ctx, u2)
		assert.Error(t, err) // Should fail on duplicate email
	})
}

// Якщо потрібна uniqueness між subtests:
func TestUserRepository_ListUsers(t *testing.T) {
	db := integration.SetupTestDBWithCleanTables(t)
	repo := postgres.NewUserRepository(db.DB)
	ctx := context.Background()

	// Створюємо кілька users - UUID в email для uniqueness
	for i := 0; i < 5; i++ {
		email := fmt.Sprintf("user_%d@example.com", i)
		u, _ := user.NewUser(email, "password123")
		require.NoError(t, repo.Create(ctx, u))
	}

	t.Run("list all users", func(t *testing.T) {
		users, total, err := repo.ListUsers(ctx, 10, 0)
		require.NoError(t, err)
		assert.Equal(t, 5, total)
		assert.Len(t, users, 5)
	})
}

Key Rules ​

DO ​

  1. Use SetupTestDBWithCleanTables(t) - TRUNCATE all tables at start of each test function
  2. Static emails for single-entity tests - No UUID pollution
  3. UUID only for intra-function uniqueness - Multiple entities in ONE test function
  4. Context first parameter - func Method(ctx context.Context, ...)
  5. require.NoError for critical checks - Stop test on setup failure
  6. assert for expectations - Continue test to see all failures

DON'T ​

  1. DON'T use SetupTestDB(t) - It doesn't clean between test functions (causes duplicate key errors)
  2. DON'T use WithTransaction - Requires repository signature changes (*sqlx.Tx vs *sqlx.DB)
  3. DON'T use CleanAllTables manually - SetupTestDBWithCleanTables already does it
  4. DON'T share state between test functions - Each function is independent
  5. DON'T use random data generators - Use predictable test data

Helper Functions ​

createTestUser (for Contact/Profile tests) ​

go
// Helper creates test user with UUID in email (for uniqueness between test functions)
func createTestUser(t *testing.T, db *integration.TestDB, userID uuidv7.UUID) {
	t.Helper()

	uniqueEmail := fmt.Sprintf("testuser_%s@example.com", userID.String())

	_, err := db.DB.Exec(`
		INSERT INTO identity_users (id, email, password_hash, status)
		VALUES ($1, $2, $3, $4)
	`, userID, uniqueEmail, "password_hash", "active")
	require.NoError(t, err)
}

func TestContactRepository_Create(t *testing.T) {
	db := integration.SetupTestDBWithCleanTables(t)
	repo := postgres.NewContactRepository(db.DB)
	ctx := context.Background()

	// Create test user first (for FK constraint)
	userID := uuidv7.New()
	createTestUser(t, db, userID)

	t.Run("create email contact", func(t *testing.T) {
		c, _ := contact.NewEmailContact(userID, "work@example.com", "Work")
		err := repo.Create(ctx, c)
		require.NoError(t, err)
	})
}

Running Integration Tests ​

bash
# All integration tests (auto-starts test DB)
make test-integration

# Specific context
go test ./test/integration/contexts/identity/... -v

# Specific aggregate
go test ./test/integration/contexts/identity/user -v

# With coverage
go test ./test/integration/... -cover

Smoke Test Pattern ​

Purpose ​

Fast validation of HTTP handler contracts without database. Mock all UseCase dependencies.

Directory Structure ​

Mirror path structure:

internal/contexts/identity/user/adapter/http/
 handler.go

test/smoke/contexts/identity/user/
 handler_test.go                ← mirrors adapter/http/

Standardized Pattern ​

go
package user_test

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

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

	"github.com/basilex/promenade/internal/contexts/identity/user"
	userHTTP "github.com/basilex/promenade/internal/contexts/identity/user/adapter/http"
	"github.com/basilex/promenade/pkg/uuidv7"
)

//  ЕТАЛОННИЙ ПАТЕРН для smoke тестів:

// 1. MockUseCase - імплементує ВСІ методи з IUseCase interface
type MockUserUseCase struct {
	mock.Mock
}

func (m *MockUserUseCase) Register(ctx context.Context, email, name, password string) (*user.User, error) {
	args := m.Called(ctx, email, name, password)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*user.User), args.Error(1)
}

func (m *MockUserUseCase) GetUser(ctx context.Context, userID uuidv7.UUID) (*user.User, error) {
	args := m.Called(ctx, userID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*user.User), args.Error(1)
}

// ... implement ALL IUseCase methods

// 2. Router setup - gin test mode
func setupUserRouter() *gin.Engine {
	gin.SetMode(gin.TestMode)
	return gin.New()
}

// 3. ONE test function for entire handler
func TestUserHandler_Smoke(t *testing.T) {
	mockUC := new(MockUserUseCase)
	handler := userHTTP.NewUserHandler(mockUC)
	router := setupUserRouter()

	// Register routes
	router.POST("/users/register", handler.Register)
	router.GET("/users/:id", handler.GetByID)
	router.POST("/users/:id/suspend", handler.Suspend)

	// 4. SUBTEST for each endpoint
	t.Run("Register returns 201", func(t *testing.T) {
		userID := uuidv7.New()

		// Create mock entity (no real hashing/validation needed)
		u := &user.User{
			ID:           userID,
			PasswordHash: "hashedPassword",
			Status:       user.UserStatusActive,
		}

		// Mock usecase call
		mockUC.On("Register", mock.Anything, "test@example.com", "Test User", "password123").Return(u, nil).Once()

		// HTTP request
		reqBody := userHTTP.RegisterRequest{
			Email:    "test@example.com",
			Name:     "Test User",
			Password: "password123",
		}
		body, _ := json.Marshal(reqBody)
		req := httptest.NewRequest(http.MethodPost, "/users/register", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		w := httptest.NewRecorder()
		router.ServeHTTP(w, req)

		// Assertions
		assert.Equal(t, http.StatusCreated, w.Code, "Register should return 201")
		mockUC.AssertExpectations(t)
	})

	t.Run("GetByID returns 200", func(t *testing.T) {
		userID := uuidv7.New()
		u := &user.User{
			ID:           userID,
			PasswordHash: "hashedPassword",
			Status:       user.UserStatusActive,
		}

		mockUC.On("GetUser", mock.Anything, mock.AnythingOfType("uuid.UUID")).Return(u, nil).Once()

		req := httptest.NewRequest(http.MethodGet, "/users/"+userID.String(), nil)
		w := httptest.NewRecorder()
		router.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code, "GetByID should return 200")
		mockUC.AssertExpectations(t)
	})

	t.Run("Suspend returns 200", func(t *testing.T) {
		userID := uuidv7.New()
		u := &user.User{
			ID:           userID,
			PasswordHash: "hashedPassword",
			Status:       user.UserStatusSuspended,
		}

		//  ВАЖЛИВО: Handler може викликати КІЛЬКА методів UseCase
		// Додати mock для ВСІХ викликів
		mockUC.On("SuspendUser", mock.Anything, mock.AnythingOfType("uuid.UUID")).Return(nil).Once()
		mockUC.On("GetUser", mock.Anything, mock.AnythingOfType("uuid.UUID")).Return(u, nil).Once() // Handler retrieves updated user

		req := httptest.NewRequest(http.MethodPost, "/users/"+userID.String()+"/suspend", nil)
		w := httptest.NewRecorder()
		router.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code, "Suspend should return 200")
		mockUC.AssertExpectations(t)
	})
}

Key Rules ​

DO ​

  1. Implement ALL IUseCase methods - Mock must satisfy interface completely
  2. Use gin.TestMode - Disable debug logging
  3. ONE test function per handler - TestXxxHandler_Smoke with subtests
  4. Mock ALL usecase calls - Handler may call multiple methods (not just primary operation)
  5. Create entities directly - No need for factory methods (NewEntity)
  6. AssertExpectations in each subtest - Verify all mocks were called

DON'T ​

  1. DON'T use real database - Smoke tests are mock-only
  2. DON'T call factory methods - Create entity structs directly
  3. DON'T forget secondary mock calls - Many handlers call GetEntity after main operation
  4. DON'T test business logic - Only HTTP contract (status codes, request/response)
  5. DON'T use testify/suite - Keep it simple with subtests

Critical Pattern: Multiple Mock Calls ​

Many handlers call UseCase method + GetEntity to return updated entity:

go
// Handler.go
func (h *UserHandler) VerifyEmail(c *gin.Context) {
	// Primary operation
	err := h.usecase.VerifyEmail(ctx, userID)
	if err != nil {
		// handle error
	}

	// SECOND CALL - retrieve updated user
	user, err := h.usecase.GetUser(ctx, userID)
	if err != nil {
		// handle error
	}

	response.Success(c, ToUserResponse(user))
}

Smoke test MUST mock BOTH calls:

go
t.Run("VerifyEmail returns 200", func(t *testing.T) {
	userID := uuidv7.New()
	u := &user.User{ID: userID, Status: user.UserStatusActive}

	//  Mock PRIMARY call
	mockUC.On("VerifyEmail", mock.Anything, mock.AnythingOfType("uuid.UUID")).Return(nil).Once()

	//  Mock SECONDARY call (handler retrieves updated user)
	mockUC.On("GetUser", mock.Anything, mock.AnythingOfType("uuid.UUID")).Return(u, nil).Once()

	req := httptest.NewRequest(http.MethodPost, "/users/"+userID.String()+"/verify-email", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	mockUC.AssertExpectations(t)
})

Running Smoke Tests ​

bash
# All smoke tests
make test-smoke

# Specific context
go test ./test/smoke/contexts/identity/... -v

# Specific aggregate
go test ./test/smoke/contexts/identity/user -v

Unit Test Pattern ​

Purpose ​

Test business logic in isolation - entities, use cases, value objects.

Location ​

In-place - same directory as production code:

internal/contexts/identity/user/
 entity.go
 entity_test.go          ← unit test
 usecase.go
 usecase_test.go         ← unit test

Entity Tests ​

go
package user

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/basilex/promenade/pkg/uuidv7"
)

func TestUser_NewUser(t *testing.T) {
	tests := []struct {
		name      string
		email     string
		password  string
		wantErr   bool
		errContains string
	}{
		{
			name:     "valid user",
			email:    "test@example.com",
			password: "ValidPass123!",
			wantErr:  false,
		},
		{
			name:        "invalid email",
			email:       "not-an-email",
			password:    "ValidPass123!",
			wantErr:     true,
			errContains: "invalid email",
		},
		{
			name:        "weak password",
			email:       "test@example.com",
			password:    "123",
			wantErr:     true,
			errContains: "password must be at least",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			user, err := NewUser(tt.email, tt.password)

			if tt.wantErr {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.errContains)
				assert.Nil(t, user)
			} else {
				require.NoError(t, err)
				assert.NotNil(t, user)
				assert.NotEqual(t, uuidv7.Nil, user.ID)
				assert.Equal(t, UserStatusActive, user.Status)
			}
		})
	}
}

func TestUser_VerifyEmail(t *testing.T) {
	u, _ := NewUser("test@example.com", "ValidPass123!")
	assert.False(t, u.IsEmailVerified)

	u.VerifyEmail()

	assert.True(t, u.IsEmailVerified)
	assert.NotNil(t, u.EmailVerifiedAt)
}

func TestUser_Suspend(t *testing.T) {
	u, _ := NewUser("test@example.com", "ValidPass123!")
	assert.Equal(t, UserStatusActive, u.Status)

	err := u.Suspend()

	require.NoError(t, err)
	assert.Equal(t, UserStatusSuspended, u.Status)
}

func TestUser_Suspend_AlreadySuspended(t *testing.T) {
	u, _ := NewUser("test@example.com", "ValidPass123!")
	u.Suspend()

	err := u.Suspend()

	assert.Error(t, err)
	assert.Contains(t, err.Error(), "already suspended")
}

UseCase Tests (with Mock Repository) ​

go
package user_test

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"

	"github.com/basilex/promenade/internal/contexts/identity/user"
	"github.com/basilex/promenade/pkg/uuidv7"
)

// MockRepository for testing
type MockRepository struct {
	mock.Mock
}

func (m *MockRepository) Create(ctx context.Context, u *user.User) error {
	args := m.Called(ctx, u)
	return args.Error(0)
}

func (m *MockRepository) GetByEmail(ctx context.Context, email string) (*user.User, error) {
	args := m.Called(ctx, email)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*user.User), args.Error(1)
}

// ... implement other methods

func TestUseCase_Register(t *testing.T) {
	mockRepo := new(MockRepository)
	uc := user.NewUseCase(mockRepo)
	ctx := context.Background()

	t.Run("register new user successfully", func(t *testing.T) {
		email := "newuser@example.com"
		name := "New User"
		password := "ValidPass123!"

		// Mock: email doesn't exist
		mockRepo.On("ExistsByEmail", ctx, email).Return(false, nil).Once()
		mockRepo.On("Create", ctx, mock.AnythingOfType("*user.User")).Return(nil).Once()

		u, err := uc.Register(ctx, email, name, password)

		require.NoError(t, err)
		assert.NotNil(t, u)
		assert.Equal(t, email, u.Email.Value())
		mockRepo.AssertExpectations(t)
	})

	t.Run("register with existing email fails", func(t *testing.T) {
		email := "existing@example.com"

		// Mock: email already exists
		mockRepo.On("ExistsByEmail", ctx, email).Return(true, nil).Once()

		u, err := uc.Register(ctx, email, "Name", "ValidPass123!")

		assert.Error(t, err)
		assert.ErrorIs(t, err, user.ErrEmailAlreadyExists)
		assert.Nil(t, u)
		mockRepo.AssertExpectations(t)
	})
}

func TestUseCase_SuspendUser(t *testing.T) {
	mockRepo := new(MockRepository)
	uc := user.NewUseCase(mockRepo)
	ctx := context.Background()

	t.Run("suspend active user", func(t *testing.T) {
		userID := uuidv7.New()
		u, _ := user.NewUser("user@example.com", "ValidPass123!")
		u.ID = userID

		mockRepo.On("GetByID", ctx, userID).Return(u, nil).Once()
		mockRepo.On("Update", ctx, u).Return(nil).Once()

		err := uc.SuspendUser(ctx, userID)

		require.NoError(t, err)
		assert.Equal(t, user.UserStatusSuspended, u.Status)
		mockRepo.AssertExpectations(t)
	})

	t.Run("suspend non-existent user fails", func(t *testing.T) {
		userID := uuidv7.New()

		mockRepo.On("GetByID", ctx, userID).Return(nil, user.ErrUserNotFound).Once()

		err := uc.SuspendUser(ctx, userID)

		assert.Error(t, err)
		assert.ErrorIs(t, err, user.ErrUserNotFound)
		mockRepo.AssertExpectations(t)
	})
}

Running Unit Tests ​

bash
# All unit tests (fast)
make test-unit

# Specific package
go test ./internal/contexts/identity/user -v

# With coverage
go test ./internal/contexts/identity/user -cover

Best Practices ​

General ​

  1. Table-Driven Tests - Use for multiple scenarios
  2. Clear Test Names - Describe expected behavior
  3. AAA Pattern - Arrange, Act, Assert
  4. One Assertion Per Test - Or use subtests
  5. No Test Interdependence - Tests can run in any order
  6. Context Propagation - Always pass ctx as first parameter

Naming ​

go
//  Good
func TestUser_NewUser_InvalidEmail_ReturnsError(t *testing.T) {}
func TestUserRepository_Create_DuplicateEmail_ReturnsError(t *testing.T) {}
func TestUserHandler_Register_MissingPassword_Returns400(t *testing.T) {}

//  Bad
func TestUser(t *testing.T) {}
func Test1(t *testing.T) {}
func TestCreate(t *testing.T) {}

Assertions ​

go
//  Use require for critical setup
require.NoError(t, err, "failed to create test user")

//  Use assert for expectations
assert.Equal(t, expected, actual)
assert.Contains(t, err.Error(), "expected substring")

//  Don't swallow errors
if err != nil {
	// Silent failure - BAD
}

Test Data ​

go
//  Predictable test data
email := "test@example.com"
name := "Test User"
password := "TestPass123!"

//  Random data (hard to debug failures)
email := fmt.Sprintf("user_%d@example.com", rand.Int())

Mock Setup ​

go
//  Clear mock expectations
mockRepo.On("Create", ctx, mock.AnythingOfType("*user.User")).Return(nil).Once()
mockRepo.AssertExpectations(t) // Verify all mocks called

//  Vague mocks
mockRepo.On("Create", mock.Anything, mock.Anything).Return(nil)

Common Pitfalls ​

Integration Tests ​

Using SetupTestDB instead of SetupTestDBWithCleanTables ​

go
//  BAD - shared DB, duplicate key errors
func TestUserRepository_Create(t *testing.T) {
	db := integration.SetupTestDB(t) // Doesn't clean between functions
	// ...
}

func TestUserRepository_Update(t *testing.T) {
	db := integration.SetupTestDB(t) // Same DB, leftover data
	// ...
}
go
//  GOOD - clean DB for each function
func TestUserRepository_Create(t *testing.T) {
	db := integration.SetupTestDBWithCleanTables(t) // TRUNCATE all tables
	// ...
}

func TestUserRepository_Update(t *testing.T) {
	db := integration.SetupTestDBWithCleanTables(t) // Fresh start
	// ...
}

Over-using UUID in emails ​

go
//  BAD - UUID pollution
func TestUserRepository_GetByID(t *testing.T) {
	db := integration.SetupTestDBWithCleanTables(t)

	email := fmt.Sprintf("test_%s@example.com", uuidv7.New().String()) // Unnecessary!
	u, _ := user.NewUser(email, "password")
	// ...
}
go
//  GOOD - static email (DB is clean)
func TestUserRepository_GetByID(t *testing.T) {
	db := integration.SetupTestDBWithCleanTables(t)

	u, _ := user.NewUser("test@example.com", "password") // Simple and clear
	// ...
}

Using WithTransaction ​

go
//  BAD - requires repository changes
func TestUserRepository_Create(t *testing.T) {
	db := integration.SetupTestDB(t)
	tx, _ := db.BeginTx(ctx, nil)
	defer tx.Rollback()

	repo := postgres.NewUserRepository(tx) // Expects *sqlx.DB not *sqlx.Tx
	// ...
}
go
//  GOOD - use SetupTestDBWithCleanTables
func TestUserRepository_Create(t *testing.T) {
	db := integration.SetupTestDBWithCleanTables(t)
	repo := postgres.NewUserRepository(db.DB) // Works with *sqlx.DB
	// ...
}

Smoke Tests ​

Forgetting secondary mock calls ​

go
//  BAD - missing GetUser mock
t.Run("VerifyEmail returns 200", func(t *testing.T) {
	mockUC.On("VerifyEmail", mock.Anything, userID).Return(nil).Once()
	// Handler ALSO calls GetUser but no mock! → PANIC
})
go
//  GOOD - mock ALL handler calls
t.Run("VerifyEmail returns 200", func(t *testing.T) {
	u := &user.User{ID: userID}
	mockUC.On("VerifyEmail", mock.Anything, userID).Return(nil).Once()
	mockUC.On("GetUser", mock.Anything, userID).Return(u, nil).Once() // Don't forget!
})

Using factory methods ​

go
//  BAD - NewUser hashes password, validates email (unnecessary in smoke test)
u, err := user.NewUser("test@example.com", "password123")
if err != nil {
	// Handle validation error in smoke test!?
}
go
//  GOOD - create struct directly
u := &user.User{
	ID:           uuidv7.New(),
	PasswordHash: "hashedPassword", // No real hashing
	Status:       user.UserStatusActive,
}

Unit Tests ​

Testing implementation details ​

go
//  BAD - testing internal methods
func TestUser_hashPassword(t *testing.T) {
	hash, err := hashPassword("password")
	// Testing private method
}
go
//  GOOD - testing public behavior
func TestUser_NewUser_PasswordIsHashed(t *testing.T) {
	u, _ := NewUser("test@example.com", "password123")
	assert.NotEqual(t, "password123", u.PasswordHash) // Verify password was hashed
}

Not using mock.AssertExpectations ​

go
//  BAD - mock setup but no verification
mockRepo.On("Create", ctx, mock.AnythingOfType("*user.User")).Return(nil)
uc.Register(ctx, "test@example.com", "Name", "password")
// Did Create get called? We don't know!
go
//  GOOD - verify all mocks called
mockRepo.On("Create", ctx, mock.AnythingOfType("*user.User")).Return(nil).Once()
uc.Register(ctx, "test@example.com", "Name", "password")
mockRepo.AssertExpectations(t) // Fails if Create not called exactly once

Test Coverage Requirements ​

Per Component ​

ComponentCoverageWhy
Entities95%+Core business logic
Use Cases85%+Business operations
Repositories80%+Data access
Handlers70%+HTTP contracts (smoke tests)
Value Objects95%+Domain primitives

Overall Project ​

  • Minimum: 80% total coverage
  • Target: 90% total coverage
  • Critical paths: 100% (authentication, authorization, payment)

Checking Coverage ​

bash
# All tests with coverage
make test-coverage

# Specific package
go test ./internal/contexts/identity/user -cover -coverprofile=coverage.out
go tool cover -html=coverage.out

# Coverage report
go test ./... -coverprofile=coverage.out -covermode=atomic
go tool cover -func=coverage.out | grep total

Additional Resources ​


Summary Checklist ​

Before writing tests, verify:

  • [ ] Correct test type (unit/smoke/integration)?
  • [ ] Using standardized pattern?
  • [ ] Mirror path structure (integration/smoke)?
  • [ ] SetupTestDBWithCleanTables for integration tests?
  • [ ] Mock ALL usecase calls for smoke tests?
  • [ ] Table-driven tests for multiple scenarios?
  • [ ] Clear, descriptive test names?
  • [ ] AssertExpectations for all mocks?
  • [ ] No UUID pollution in test data?
  • [ ] Context propagation (ctx first parameter)?

Last Updated: 2025-12-28
Status: Production-ready
Maintainer: Promenade Team

Built with Domain-Driven Design and Go