Skip to content

Testing

How to write and run tests in HexaGo-generated projects.


Automatic Test Generation with go-testgen

HexaGo integrates with go-testgen to scaffold test files for generated adapters immediately after they are written.

Install go-testgen

go install github.com/padiazg/go-testgen@latest

Minimum required version: v0.1.0. HexaGo checks the installed version before invoking it. If it is missing or outdated, a warning is printed and adapter generation continues normally.

Enable in your project

Pass --with-tests to hexago init โ€” this saves testing.enabled: true to .hexago.yaml and applies to all subsequent add commands:

hexago init my-app --module github.com/user/my-app --with-tests

Or toggle per-command with --with-test / --no-test (these override the config default):

hexago add adapter secondary database UserRepository --with-test
hexago add adapter primary  http    UserHandler      --no-test

How it works

After the adapter files are written, HexaGo:

  1. Runs go-testgen report <adapter-pkg> --format json to discover all exported functions that lack a test.
  2. For each untested function, runs the suggested go-testgen gen command โ€” including --style and --mock-from flags inferred by go-testgen automatically.
๐Ÿงช Generating test: UserRepository.FindByID
๐Ÿงช Generating test: UserRepository.Create

.hexago.yaml configuration

testing:
  enabled: true   # set by hexago init --with-tests

Requirements

  • The adapter package must compile after generation (all imported types must resolve).
    If they don't, re-run hexago add adapter ... --with-test after creating the missing domain types.
  • go-testgen runs in the project root directory (--working-directory / detected root).
  • Non-interactive: existing test functions are never overwritten.

Test Types

Type Description How to Run
Unit tests Test isolated components, no external deps go test ./...
Integration tests Test with real external services go test -tags=integration ./...

Unit tests must pass without any external service (database, API, etc.).


Running Tests

Basic Commands

# Run all tests
go test ./...

# Run with verbose output
go test -v ./...

# Run specific test
go test ./... -run TestUser_Create

# Run tests in specific package
go test ./internal/core/domain/...

# Run with race detector
go test -race ./...

Code Coverage

# Generate coverage report
go test -coverprofile=coverage.out ./...

# View HTML coverage report
go tool cover -html=coverage.out

# Show coverage by function
go tool cover -func=coverage.out

Integration Tests

# Run only integration tests
go test -tags=integration ./...

# Run with verbose output
go test -tags=integration -v ./...

Integration tests require API keys or database connections configured in .env.


Test Organization

File Naming

internal/core/domain/
โ”œโ”€โ”€ user.go
โ”œโ”€โ”€ user_test.go      โ† unit tests
โ””โ”€โ”€ user_integration_test.go  โ† integration tests (build tag)

Test Structure

Follow AAA pattern (Arrange, Act, Assert):

func TestUserService_Create(t *testing.T) {
    // Arrange - set up test data and mocks
    mockStore := &MockStore{}
    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
    service := NewUserService(mockStore, logger)

    req := &CreateUserRequest{
        Email: "test@example.com",
        Name:  "Test User",
    }

    // Act - execute the function under test
    result, err := service.Create(context.Background(), req)

    // Assert - verify the outcome
    require.NoError(t, err)
    assert.Equal(t, "test@example.com", result.Email)
    assert.Equal(t, "Test User", result.Name)
}

Test Dependencies

Testify

Use testify for assertions and mocking:

import (
    "github.com/stretchr/testify/assert"   // non-fatal assertions
    "github.com/stretchr/testify/require"   // fatal assertions
    "github.com/stretchr/testify/mock"     // mocking
)
Package Use Case
assert Continue on failure, good for multiple checks
require Stop on failure, good for prerequisites
mock Create mock objects for interfaces

Example with Mocks

type MockStore struct {
    mock.Mock
}

func (m *MockStore) GetUser(ctx context.Context, id string) (*User, error) {
    args := m.Called(ctx, id)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*User), args.Error(1)
}

func TestGetUser(t *testing.T) {
    mockStore := new(MockStore)

    // Set up expectations
    mockStore.On("GetUser", mock.Anything, "123").Return(&User{ID: "123"}, nil)

    service := NewUserService(mockStore)
    user, err := service.GetUser(context.Background(), "123")

    require.NoError(t, err)
    assert.Equal(t, "123", user.ID)

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

Build Tags

Use build tags to separate unit and integration tests:

//go:build integration

package myapp_test

// Integration tests require real services
func TestRealDatabase(t *testing.T) {
    // This test only runs with -tags=integration
}
// Unit tests - no build tag needed
package myapp_test

func TestUnit(t *testing.T) {
    // This test always runs
}

Test Utilities

Test Fixtures

func TestMain(m *testing.M) {
    // Setup before all tests
    os.Exit(m.Run())
    // Teardown after all tests
}

Helper Functions

func mustCreateUser(t *testing.T, email string) *User {
    user := &User{
        ID:    uuid.New(),
        Email: email,
    }
    require.NoError(t, user.Validate())
    return user
}

Table-Driven Tests

For testing multiple input combinations:

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name  string
        email string
        valid bool
    }{
        {"valid email", "test@example.com", true},
        {"invalid - no @", "testexample.com", false},
        {"invalid - no domain", "test@", false},
        {"empty", "", false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateEmail(tt.email)
            if tt.valid {
                assert.NoError(t, err)
            } else {
                assert.Error(t, err)
            }
        })
    }
}

Best Practices

  1. Test behavior, not implementation โ€” Test public interfaces
  2. Name tests descriptively โ€” TestService_Create_ValidInput
  3. One assertion per test is not required โ€” Group related assertions
  4. Use require for prerequisites โ€” Fail fast on missing preconditions
  5. Use assert for actual checks โ€” Continue to see all failures
  6. No external deps in unit tests โ€” Mock everything external
  7. Clean up in tests โ€” Use t.Cleanup() for resources