Skip to content

Integration testing

This page covers the different test suites in this repo:

  • Go tests (all): make test (runs go test ./...)
  • Go unit-ish tests: make test-unit (runs go test -short ./...)
  • Go HTTP integration tests: make test-integration (runs go test ./internal/infrastructure/http/server/... with an in-memory SQLite DB + embedded goose migrations)
  • Browser E2E tests (Playwright): cd e2e && npm test (real browser against a real server process + temp, file-backed SQLite DB)
Terminal window
make test # all Go tests
make test-unit # fast-ish: `go test -short ./...`
make test-integration # HTTP integration tests (SQLite in-memory)

Prereqs:

  • Go
  • Node.js
  • sqlite3 available on your PATH (used for DB assertions)
Terminal window
make generate
cd e2e
npm ci
npx playwright install chromium
npm test

Linux CI note: npx playwright install --with-deps chromium.

Run all HTTP integration tests:

Terminal window
make test-integration

Run end-to-end workflow tests:

Terminal window
go test -v -run TestE2E ./internal/infrastructure/http/server/

Run API endpoint tests:

Terminal window
go test -v -run TestAPI ./internal/infrastructure/http/server/

Run authentication tests:

Terminal window
go test -v -run Auth ./internal/infrastructure/http/server/

Run redirect and analytics tests:

Terminal window
go test -v -run Redirect ./internal/infrastructure/http/server/

End-to-End Tests (e2e_integration_test.go)

Section titled “End-to-End Tests (e2e_integration_test.go)”

Comprehensive workflow tests that verify the complete user journey:

Tests the complete workflow:

  1. Authenticate - Create URL with valid auth token
  2. Create URL - Generate a shortened URL
  3. Redirect - Follow the short URL to the original
  4. Verify Analytics - Confirm click tracking works

What it validates:

  • Authentication middleware
  • URL creation with database persistence
  • HTTP redirects with proper status codes
  • Asynchronous click recording
  • Analytics data aggregation

Tests authentication scenarios:

  • Valid token acceptance
  • Invalid token rejection
  • Missing token handling
  • Malformed header rejection

What it validates:

  • Auth middleware behavior
  • Token validation logic
  • Error response formats

Tests error handling:

  • Nonexistent short codes (404)
  • Invalid URL formats (400)
  • Empty URLs (400)
  • Malformed JSON (400)
  • Analytics for missing URLs (404)

What it validates:

  • Input validation
  • Error responses
  • HTTP status codes
  • Error message formats

Tests all API endpoints:

  • POST /api/urls - Create URL
  • GET /api/urls - List URLs
  • GET /api/urls/{shortCode}/analytics - Get analytics
  • DELETE /api/urls/{shortCode} - Delete URL

What it validates:

  • Complete API surface
  • Request/response formats
  • CRUD operations
  • Data persistence

Tests analytics with multiple redirects:

  • Multiple clicks from different referrers
  • Async click recording
  • Analytics aggregation

What it validates:

  • Click tracking accuracy
  • Referrer tracking
  • Async worker queue
  • Analytics computation

Tests concurrent URL creation:

  • 10 simultaneous URL creation requests
  • Database concurrency handling

What it validates:

  • Thread safety
  • Database locking
  • Race condition handling

Tests health check endpoint:

  • Successful response
  • No authentication required

What it validates:

  • Monitoring endpoints
  • Public access routes

API Integration Tests (api_integration_test.go)

Section titled “API Integration Tests (api_integration_test.go)”
  • Detailed API endpoint testing
  • Request validation
  • Response format verification

Auth Integration Tests (auth_integration_test.go)

Section titled “Auth Integration Tests (auth_integration_test.go)”
  • Authentication middleware
  • Protected vs public routes
  • User context propagation

Redirect Integration Tests (redirect_integration_test.go)

Section titled “Redirect Integration Tests (redirect_integration_test.go)”
  • URL redirection logic
  • Click tracking
  • Analytics recording

Analytics Integration Tests (analytics_integration_test.go)

Section titled “Analytics Integration Tests (analytics_integration_test.go)”
  • Analytics computation
  • Time range filtering
  • Data aggregation

Server Integration Tests (integration_test.go)

Section titled “Server Integration Tests (integration_test.go)”
  • Middleware execution order
  • CORS handling
  • Concurrent requests
  • Graceful shutdown

All integration tests use in-memory SQLite databases for isolation and speed:

db := setupTestDB(t)
defer db.Close()

The setupTestDB helper:

  1. Creates an in-memory SQLite database (:memory:)
  2. Applies all migrations using goose
  3. Returns a ready-to-use database connection

Benefits:

  • Fast - No disk I/O
  • Isolated - Each test gets a fresh database
  • Portable - No external dependencies
  • Deterministic - Clean state for every test

Most tests use table-driven patterns for comprehensive scenario coverage:

tests := []struct {
name string
authHeader string
expectedStatus int
checkResponse func(t *testing.T, body map[string]interface{})
}{
{
name: "valid_token",
authHeader: "Bearer test-token",
expectedStatus: http.StatusCreated,
},
// More test cases...
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test logic
})
}

Related scenarios are grouped as subtests:

t.Run("create_url", func(t *testing.T) {
// Create URL test
t.Run("redirect", func(t *testing.T) {
// Redirect test using created URL
})
})

Common test utilities:

  • setupTestDB(t) - Create test database
  • testLogger() - Get a disabled logger for tests
  • Standard Go testing helpers
  • Each test creates its own database
  • No shared state between tests
  • Independent test execution

For async operations like click recording, use polling with a deadline instead of a fixed sleep:

// Trigger redirect
srv.router.ServeHTTP(rec, req)
// Wait for async processing using polling with a deadline
deadline := time.Now().Add(2 * time.Second)
for {
// Check condition (e.g., click recorded in analytics)
analyticsReq := httptest.NewRequest(http.MethodGet, "/api/urls/"+shortCode+"/analytics", nil)
analyticsReq.Header.Set("Authorization", "Bearer test-token")
analyticsRec := httptest.NewRecorder()
srv.router.ServeHTTP(analyticsRec, analyticsReq)
if analyticsRec.Code == http.StatusOK {
var analytics map[string]interface{}
if err := json.Unmarshal(analyticsRec.Body.Bytes(), &analytics); err == nil {
if totalClicks, ok := analytics["total_clicks"].(float64); ok && totalClicks > 0 {
break
}
}
}
if time.Now().After(deadline) {
t.Fatalf("timeout waiting for click to be recorded")
}
time.Sleep(10 * time.Millisecond)
}

Use proper synchronization:

results := make(chan error, numRequests)
for i := 0; i < numRequests; i++ {
go func(index int) {
// Test logic
results <- nil
}(i)
}
// Collect results
for i := 0; i < numRequests; i++ {
if err := <-results; err != nil {
t.Error(err)
}
}

Provide context in error messages:

if rec.Code != tt.expectedStatus {
t.Errorf("expected status %d, got %d. Body: %s",
tt.expectedStatus, rec.Code, rec.Body.String())
}

CI runs on GitHub Actions (see .github/workflows/ci.yml) and executes go test ./... (plus separate formatting/vet/lint and generated-code checks).

Integration tests live under internal/infrastructure/http/server/ and can be run locally with:

Terminal window
make test-integration

Docker Compose is intended for manual local testing with a persistent SQLite database (bind-mounted to ./data). See Docker Compose testing.

To get a coverage report for the HTTP integration suite:

Terminal window
go test -cover ./internal/infrastructure/http/server/

Detailed coverage:

Terminal window
go test -coverprofile=coverage.out ./internal/infrastructure/http/server/
go tool cover -html=coverage.out

Cause: SQLite doesn’t handle high concurrency well in tests. Fix: Reduce concurrent operations or add delays between operations.

Cause: Not waiting long enough for async operations. Fix: Prefer polling with a deadline (the suite already uses this pattern); increase the polling deadline if needed.

Cause: Tests using real HTTP server instead of httptest. Fix: Use httptest.NewRecorder() instead of real HTTP server.

Place in internal/infrastructure/http/server/:

package server
import (
"testing"
// imports...
)
  • File: *_integration_test.go or *_test.go
  • Function: TestE2E_* or Test*Integration
func TestYourFeature(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
cfg := &config.Config{
ServerPort: 8080,
BaseURL: "http://localhost:8080",
DatabaseURL: "test.db",
AuthToken: "test-token",
AllowedOrigins: "*",
}
srv, err := New(cfg, db, testLogger())
if err != nil {
t.Fatalf("failed to create server: %v", err)
}
defer srv.Shutdown(context.Background())
// Test logic here
}
req := httptest.NewRequest(http.MethodPost, "/api/urls", body)
req.Header.Set("Authorization", "Bearer test-token")
rec := httptest.NewRecorder()
srv.router.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Errorf("expected %d, got %d", http.StatusCreated, rec.Code)
}

Always use defer for cleanup:

db := setupTestDB(t)
defer db.Close()
srv, _ := New(cfg, db, logger)
defer srv.Shutdown(context.Background())

Integration tests include benchmarks:

Terminal window
# Run benchmarks
go test -bench=. ./internal/infrastructure/http/server/
# With memory profiling
go test -bench=. -benchmem ./internal/infrastructure/http/server/

Example benchmark results:

BenchmarkServer_HealthCheck-8 500000 2314 ns/op
BenchmarkServer_WithMiddleware-8 200000 8745 ns/op

The integration testing suite provides:

Comprehensive Coverage - All API endpoints and workflows tested ✅ Fast Execution - In-memory database, ~1 second total ✅ Reliable - Designed to minimize flakiness with deterministic assertions (some tests use time-based polling and may be timing-sensitive on heavily loaded environments) ✅ CI/CD Ready - No external dependencies required ✅ Well Organized - Clear test structure and naming ✅ Documented - Examples and patterns for new tests ✅ Maintainable - Helper functions and consistent patterns

The tests serve as both quality assurance and living documentation of system behavior.