New test files: - config/config_test.go (100% coverage) - constants/constants_test.go (100% coverage) - httputil/response_test.go (100% coverage) - validation/rules_test.go (91.9% coverage) - middleware/logger_test.go, security_test.go, security_logger_test.go - handlers/errors_test.go Updated documentation: - doc/27-GO-TESTING.md: Complete testing guide - doc/00-GO-DOCUMENTATION-INDEX.md: Added testing section - doc/01-ARCHITECTURE.md: Updated package structure - doc/DECISIONS.md: Added ADR-004 caching decision - PROJECT-MEMORY.md: Added Go testing section
14 KiB
Go Testing Documentation
Comprehensive guide to the testing infrastructure of the CV site Go backend.
Table of Contents
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 responseJSONOk()- Success JSON responseJSONCached()- Cached JSON responseHTML()- HTML response with proper headersNoContent()- 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:
responseWriterimplementationWriteHeader()status captureWrite()body capture- Middleware integration
internal/middleware/contact_rate_limit_test.go
Rate limiting testing:
NewContactRateLimiter()initializationallow()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()functiongetSeverity()mappingSecurityLoggermiddlewareisSecurityRelevantPath()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 handlingruleTrim- Whitespace trimming markerruleSanitize- HTML sanitization markerruleMin- Minimum length validation (UTF-8 aware)ruleTiming- Bot detection timingFieldError.Error()- Error formattingValidationErrors.HasErrors()- Error checkingValidationErrors.GetFieldErrors()- Field-specific errors
internal/handlers/errors_test.go
Error handling testing:
AppError.Error()- Error message formattingNewAppError()- Error constructorHandleError()with JSON requestsHandleError()with HTMX requestsHandleError()with standard requests- Internal error message hiding
- Error constructors:
NotFoundError(),BadRequestError()InternalError(),TemplateError()DataLoadError()
DomainErrortype testing- Method chaining (
WithError(),WithField())
Running Tests
Basic Commands
# 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
# 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
# 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
# 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:
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:
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:
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:
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
// 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
// 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
// Good: Create fresh instance per test
func TestRateLimiter(t *testing.T) {
rl := &ContactRateLimiter{
clients: make(map[string]*contactRateLimitEntry),
}
// test code
}
5. Test Error Messages
if !strings.Contains(body, "Too Many Requests") {
t.Error("Response should contain error message")
}
6. Use Constants from Production Code
// Good: Use production constants
limit := c.RateLimitContactRequests
// Bad: Hardcode values
limit := 5
7. Check Response Headers
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
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
#!/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 - Validation system details
- 25-GO-TEMPLATE-SYSTEM.md - Template system details
- 26-GO-ROUTES-API.md - Routes and API documentation
- 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