# Go Testing Documentation Comprehensive guide to the testing infrastructure of the CV site Go backend. ## Table of Contents 1. [Coverage Summary](#coverage-summary) 2. [Test Files](#test-files) 3. [Running Tests](#running-tests) 4. [Test Patterns](#test-patterns) 5. [Coverage Gaps](#coverage-gaps) 6. [Best Practices](#best-practices) --- ## Coverage Summary Current test coverage as of December 2025: | Package | Coverage | Status | Notes | |---------|----------|--------|-------| | `internal/config` | **100%** | Excellent | Fully tested configuration loading | | `internal/constants` | **100%** | Excellent | All constants and validation values | | `internal/httputil` | **100%** | Excellent | All response helper functions | | `internal/cache` | **95.7%** | Excellent | Application-level data caching | | `internal/validation` | **91.9%** | Excellent | Validation rules and error handling | | `internal/middleware` | **87.5%** | Good | Security, rate limiting, preferences | | `internal/fileutil` | **88.9%** | Good | File path utilities | | `internal/models/ui` | **85.7%** | Good | UI configuration models | | `internal/models/cv` | **83.3%** | Good | CV data models | | `internal/handlers` | **62.9%** | Fair | HTTP handlers (PDF requires Chrome) | | `internal/email` | **58.0%** | Fair | Email requires SMTP connection | | `internal/pdf` | **0%** | N/A | Requires Chrome/chromedp | | `internal/templates` | **0%** | N/A | File-system dependent | | `internal/routes` | **0%** | N/A | Integration testing required | | `internal/models` | **0%** | N/A | Interface-only package | **Overall Project Coverage: ~70-75%** (for testable packages) --- ## Test Files ### High-Coverage Packages (90%+) #### `internal/config/config_test.go` Tests for application configuration loading: - Environment variable parsing - Default value handling - Port configuration - SMTP settings validation #### `internal/constants/constants_test.go` Tests for constant values and validation: - Language constants (English, Spanish) - CV theme constants (default, clean) - CV length constants (short, long) - Color theme constants (light, dark) - Rate limit configurations - HTTP header constants #### `internal/httputil/response_test.go` Tests for HTTP response helpers: - `JSON()` - Generic JSON response - `JSONOk()` - Success JSON response - `JSONCached()` - Cached JSON response - `HTML()` - HTML response with proper headers - `NoContent()` - 204 No Content response ### Good-Coverage Packages (80-90%) #### `internal/middleware/csrf_test.go` CSRF protection testing: - Token generation (`generateToken`) - Token validation (`validateToken`) - `GetToken()` from request context - Middleware protection flow - HTMX request handling #### `internal/middleware/logger_test.go` Request logging testing: - `responseWriter` implementation - `WriteHeader()` status capture - `Write()` body capture - Middleware integration #### `internal/middleware/contact_rate_limit_test.go` Rate limiting testing: - `NewContactRateLimiter()` initialization - `allow()` function behavior - Middleware blocking behavior - HTMX error responses - X-Forwarded-For header handling - X-Real-IP header handling - `GetStats()` statistics #### `internal/middleware/security_logger_test.go` Security logging and preferences testing: - `LogSecurityEvent()` function - `getSeverity()` mapping - `SecurityLogger` middleware - `isSecurityRelevantPath()` detection - Preferences helper functions: - `GetLanguage()`, `GetCVLength()`, `GetCVIcons()` - `GetCVTheme()`, `GetColorTheme()` - `IsLongCV()`, `IsShortCV()` - `ShowIcons()`, `HideIcons()` - `IsCleanTheme()`, `IsDefaultTheme()` - `IsDarkMode()`, `IsLightMode()` #### `internal/validation/rules_test.go` Validation rules testing: - `ruleOptional` - Optional field handling - `ruleTrim` - Whitespace trimming marker - `ruleSanitize` - HTML sanitization marker - `ruleMin` - Minimum length validation (UTF-8 aware) - `ruleTiming` - Bot detection timing - `FieldError.Error()` - Error formatting - `ValidationErrors.HasErrors()` - Error checking - `ValidationErrors.GetFieldErrors()` - Field-specific errors #### `internal/handlers/errors_test.go` Error handling testing: - `AppError.Error()` - Error message formatting - `NewAppError()` - Error constructor - `HandleError()` with JSON requests - `HandleError()` with HTMX requests - `HandleError()` with standard requests - Internal error message hiding - Error constructors: - `NotFoundError()`, `BadRequestError()` - `InternalError()`, `TemplateError()` - `DataLoadError()` - `DomainError` type testing - Method chaining (`WithError()`, `WithField()`) --- ## Running Tests ### Basic Commands ```bash # Run all tests go test ./... # Run with coverage go test -cover ./... # Run specific package go test ./internal/middleware/... # Run with verbose output go test -v ./internal/validation/... # Run with race detection go test -race ./... ``` ### Coverage Report ```bash # Generate coverage profile go test -coverprofile=coverage.out ./internal/... # View in terminal go tool cover -func=coverage.out # Generate HTML report go tool cover -html=coverage.out -o coverage.html # Open HTML report (macOS) open coverage.html ``` ### Package-Specific Testing ```bash # Config tests go test -v ./internal/config/ # Middleware tests go test -v ./internal/middleware/ # Validation tests go test -v ./internal/validation/ # Handler tests go test -v ./internal/handlers/ # All tests with coverage summary go test -cover ./internal/... 2>&1 | grep -E "^ok|coverage:" ``` ### Running Individual Tests ```bash # Run tests matching a pattern go test -v -run "TestCSRF" ./internal/middleware/ # Run specific test function go test -v -run "TestRuleMin" ./internal/validation/ # Run subtests go test -v -run "TestValidationErrors_GetFieldErrors/Get_multiple" ./internal/validation/ ``` --- ## Test Patterns ### Table-Driven Tests Most tests use Go's table-driven test pattern for comprehensive coverage: ```go func TestRuleMin(t *testing.T) { tests := []struct { name string field string value string param string hasError bool }{ {"Valid - meets minimum", "msg", "hello", "5", false}, {"Valid - exceeds minimum", "msg", "hello world", "5", false}, {"Invalid - too short", "msg", "hi", "5", true}, {"UTF-8 aware - valid", "name", "Jose", "4", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ruleMin(tt.field, tt.value, tt.param) if (result != nil) != tt.hasError { t.Errorf("ruleMin(%q, %q, %q) error = %v, wantError %v", tt.field, tt.value, tt.param, result != nil, tt.hasError) } }) } } ``` ### HTTP Handler Testing Using `net/http/httptest` for handler tests: ```go func TestHandleError_JSON(t *testing.T) { appErr := NewAppError(nil, "Bad request", http.StatusBadRequest, false) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(c.HeaderAccept, c.ContentTypeJSON) rec := httptest.NewRecorder() HandleError(rec, req, appErr) if rec.Code != http.StatusBadRequest { t.Errorf("Status = %d, want %d", rec.Code, http.StatusBadRequest) } var response ErrorResponse if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to parse JSON response: %v", err) } } ``` ### Context-Based Testing Testing preferences via request context: ```go func TestPreferencesHelperFunctions(t *testing.T) { prefs := &Preferences{ CVLength: c.CVLengthLong, CVIcons: c.CVIconsShow, CVLanguage: c.LangSpanish, CVTheme: c.CVThemeClean, ColorTheme: c.ColorThemeDark, } req := httptest.NewRequest(http.MethodGet, "/", nil) ctx := context.WithValue(req.Context(), PreferencesKey, prefs) reqWithPrefs := req.WithContext(ctx) t.Run("GetLanguage", func(t *testing.T) { result := GetLanguage(reqWithPrefs) if result != c.LangSpanish { t.Errorf("GetLanguage() = %q, want %q", result, c.LangSpanish) } }) } ``` ### Middleware Testing Testing middleware chains: ```go func TestContactRateLimiter_Middleware_Blocked(t *testing.T) { rl := &ContactRateLimiter{ clients: make(map[string]*contactRateLimitEntry), } handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) protected := rl.Middleware(handler) // Exhaust the rate limit limit := c.RateLimitContactRequests for i := 0; i < limit; i++ { req := httptest.NewRequest(http.MethodPost, "/api/contact", nil) req.RemoteAddr = "192.168.1.1:12345" rec := httptest.NewRecorder() protected.ServeHTTP(rec, req) } // Next request should be blocked req := httptest.NewRequest(http.MethodPost, "/api/contact", nil) req.RemoteAddr = "192.168.1.1:12345" rec := httptest.NewRecorder() protected.ServeHTTP(rec, req) if rec.Code != http.StatusTooManyRequests { t.Errorf("Status = %d, want %d", rec.Code, http.StatusTooManyRequests) } } ``` --- ## Coverage Gaps ### Why Some Packages Have 0% Coverage #### `internal/pdf` (0%) - **Reason**: Requires Chrome browser via chromedp - **Solution**: Would need headless Chrome in CI/CD - **Alternative**: Mock the chromedp interface (significant refactoring) #### `internal/templates` (0%) - **Reason**: File-system dependent template loading - **Solution**: Could use embedded test templates - **Impact**: Low priority - simple template wrapper #### `internal/routes` (0%) - **Reason**: Integration-level routing setup - **Solution**: End-to-end testing with running server - **Alternative**: Test individual handlers instead #### `internal/models` (0%) - **Reason**: Contains only interface definitions - **Impact**: None - interfaces have no executable code ### Partial Coverage Explanations #### `internal/middleware` (87.5%) Uncovered code includes: - Background goroutine cleanup functions (tickers) - Production-only file logging (`/var/log/`) - Edge cases in recovery middleware #### `internal/email` (58.0%) Uncovered code includes: - Actual SMTP connection and sending - TLS handshake code - Network error handling **Note**: Would require SMTP mock server #### `internal/handlers` (62.9%) Uncovered code includes: - PDF generation handlers (need Chrome) - Some HTMX-specific response paths - Error paths for template loading failures --- ## Best Practices ### 1. Use Table-Driven Tests ```go // Good: Table-driven tests := []struct { name string input string expected bool }{ {"valid", "test@example.com", true}, {"invalid", "not-an-email", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // test code }) } ``` ### 2. Test Edge Cases Always test: - Empty inputs - Maximum length inputs - Unicode/UTF-8 characters - Invalid parameters - Boundary conditions ### 3. Use Descriptive Test Names ```go // Good t.Run("UTF-8 aware - valid Japanese characters", ...) t.Run("Invalid - exceeds maximum length", ...) // Bad t.Run("test1", ...) t.Run("case2", ...) ``` ### 4. Isolate Tests ```go // Good: Create fresh instance per test func TestRateLimiter(t *testing.T) { rl := &ContactRateLimiter{ clients: make(map[string]*contactRateLimitEntry), } // test code } ``` ### 5. Test Error Messages ```go if !strings.Contains(body, "Too Many Requests") { t.Error("Response should contain error message") } ``` ### 6. Use Constants from Production Code ```go // Good: Use production constants limit := c.RateLimitContactRequests // Bad: Hardcode values limit := 5 ``` ### 7. Check Response Headers ```go contentType := rec.Header().Get(c.HeaderContentType) if contentType != c.ContentTypeJSON { t.Errorf("Content-Type = %q, want %q", contentType, c.ContentTypeJSON) } ``` --- ## Test File Structure ``` internal/ ├── cache/ │ └── cache_test.go # Data caching tests ├── config/ │ └── config_test.go # Configuration tests ├── constants/ │ └── constants_test.go # Constants validation ├── email/ │ └── email_test.go # Email service tests ├── fileutil/ │ └── fileutil_test.go # File utilities tests ├── handlers/ │ └── errors_test.go # Error handling tests ├── httputil/ │ └── response_test.go # HTTP response tests ├── middleware/ │ ├── csrf_test.go # CSRF protection tests │ ├── logger_test.go # Logging middleware tests │ ├── contact_rate_limit_test.go # Rate limiting tests │ └── security_logger_test.go # Security logging tests ├── models/ │ ├── cv/ │ │ └── cv_test.go # CV model tests │ └── ui/ │ └── ui_test.go # UI model tests └── validation/ ├── validator_test.go # Core validator tests └── rules_test.go # Validation rules tests ``` --- ## CI/CD Integration ### GitHub Actions Example ```yaml name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.23' - name: Run tests run: go test -v -race -coverprofile=coverage.out ./... - name: Check coverage run: | go tool cover -func=coverage.out | grep total | awk '{print $3}' ``` ### Pre-commit Hook ```bash #!/bin/bash # .git/hooks/pre-commit echo "Running tests..." go test ./internal/... -cover if [ $? -ne 0 ]; then echo "Tests failed. Commit aborted." exit 1 fi ``` --- ## Related Documentation - [24-GO-VALIDATION-SYSTEM.md](24-GO-VALIDATION-SYSTEM.md) - Validation system details - [25-GO-TEMPLATE-SYSTEM.md](25-GO-TEMPLATE-SYSTEM.md) - Template system details - [26-GO-ROUTES-API.md](26-GO-ROUTES-API.md) - Routes and API documentation - [00-GO-DOCUMENTATION-INDEX.md](00-GO-DOCUMENTATION-INDEX.md) - Go documentation index --- **Last Updated:** December 6, 2025 **Total Test Files:** 12 **Tested Packages:** 11 (with meaningful coverage) **Overall Coverage:** ~70-75% for testable code