Skip to main content

Testing

Test Stats (Phase 1)

  • 29 test functions across 10 test files
  • 166 test cases (table-driven)
  • All passing, race-clean
  • Coverage target: 70% minimum

Running Tests

# All unit tests
make test

# With race detector
make test-race

# With coverage report
make test-coverage
# Opens coverage.html in browser

# Integration tests (requires Docker services)
make test-integration

# E2E smoke tests
make test-e2e

Test Patterns

Table-Driven Tests

All tests use table-driven patterns with testify/assert and testify/require:

func TestCalculateFinancials(t *testing.T) {
tests := []struct {
name string
dealType string
hours decimal.Decimal
expected decimal.Decimal
}{
{
name: "SUP within limit",
dealType: "SUP",
hours: decimal.NewFromFloat(30),
expected: decimal.NewFromFloat(15000000),
},
{
name: "SUP with overtime",
dealType: "SUP",
hours: decimal.NewFromFloat(50),
expected: decimal.NewFromFloat(16500000),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := calculateFinancials(tt.dealType, tt.hours)
assert.True(t, tt.expected.Equal(result.TotalAmount))
})
}
}

Handler Mocks

Handlers use function-field structs (not codegen) for mocking:

type mockInvoiceService struct {
GetByIDFunc func(ctx context.Context, id uuid.UUID) (*models.Invoice, error)
GenerateFunc func(ctx context.Context, req models.GenerateRequest) (*models.Invoice, error)
FinalizeFunc func(ctx context.Context, id uuid.UUID) (*models.Invoice, error)
}

func (m *mockInvoiceService) GetByID(ctx context.Context, id uuid.UUID) (*models.Invoice, error) {
return m.GetByIDFunc(ctx, id)
}

Database Mocking

Use go-sqlmock for PostgreSQL layer tests:

func TestGetInvoice(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()

mock.ExpectQuery("SELECT .* FROM invoices").
WithArgs(invoiceID).
WillReturnRows(sqlmock.NewRows(columns).AddRow(values...))

// ... test logic
}

PDF Integration Test

A separate integration test validates PDF generation against the real Gotenberg instance:

# Requires access to convert.prxm.uz
go test ./internal/services/generator/ -tags=integration -run TestPDFGeneration

Test File Locations

Tests live next to the code they test:

internal/
handlers/
invoice_handler.go
invoice_handler_test.go
services/
invoice/
service.go
service_test.go
generator/
generator.go
generator_test.go
pdf_test.go
models/
invoice_test.go

CI Integration

Tests run in GitLab CI on every MR:

  1. make lint — golangci-lint + ESLint
  2. make test-race — Unit tests with race detector
  3. make test-coverage — Coverage report (must exceed 70%)
  4. JUnit XML reports in MR diffs