Testing
8 minute read
This guide covers testing utilities provided by the metrics package.
Testing Utilities
The metrics package provides utilities for testing without port conflicts or complex setup.
TestingRecorder
Create a test recorder with stdout provider. No network is required.
package myapp_test
import (
"testing"
"rivaas.dev/metrics"
)
func TestHandler(t *testing.T) {
t.Parallel()
// Create test recorder (uses stdout, avoids port conflicts)
recorder := metrics.TestingRecorder(t, "test-service")
// Use recorder in tests...
handler := NewHandler(recorder)
// Test your handler
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
// Assertions...
// Cleanup is automatic via t.Cleanup()
}
// With additional options
func TestWithOptions(t *testing.T) {
recorder := metrics.TestingRecorder(t, "test-service",
metrics.WithMaxCustomMetrics(100),
)
// ...
}
Signature
func TestingRecorder(tb testing.TB, serviceName string, opts ...Option) *Recorder
Parameters:
tb testing.TB- Test or benchmark instance.serviceName string- Service name for metrics.opts ...Option- Optional additional configuration options.
Features
- No port conflicts: Uses stdout provider, no network required.
- Automatic cleanup: Registers cleanup via
t.Cleanup(). - Parallel safe: Safe to use in parallel tests.
- Simple setup: One-line initialization.
- Works with benchmarks: Accepts
testing.TB(both*testing.Tand*testing.B).
Example
func TestMetricsCollection(t *testing.T) {
t.Parallel()
recorder := metrics.TestingRecorder(t, "test-service")
// Record some metrics
ctx := context.Background()
err := recorder.IncrementCounter(ctx, "test_counter")
if err != nil {
t.Errorf("Failed to record counter: %v", err)
}
err = recorder.RecordHistogram(ctx, "test_duration", 1.5)
if err != nil {
t.Errorf("Failed to record histogram: %v", err)
}
// Test passes if no errors
}
TestingRecorderWithPrometheus
Create a test recorder with Prometheus provider (for endpoint testing):
func TestPrometheusEndpoint(t *testing.T) {
t.Parallel()
// Create test recorder with Prometheus (dynamic port)
recorder := metrics.TestingRecorderWithPrometheus(t, "test-service")
// Wait for server to be ready
err := metrics.WaitForMetricsServer(t, recorder.ServerAddress(), 5*time.Second)
if err != nil {
t.Fatal(err)
}
// Test metrics endpoint (note: ServerAddress returns port like ":9090")
resp, err := http.Get("http://localhost" + recorder.ServerAddress() + "/metrics")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}
}
Signature
func TestingRecorderWithPrometheus(tb testing.TB, serviceName string, opts ...Option) *Recorder
Parameters:
tb testing.TB- Test or benchmark instanceserviceName string- Service name for metricsopts ...Option- Optional additional configuration options
Features
- Dynamic port allocation: Automatically finds available port
- Real Prometheus endpoint: Test actual HTTP metrics endpoint
- Server readiness check: Use
WaitForMetricsServerto wait for startup - Automatic cleanup: Shuts down server via
t.Cleanup() - Works with benchmarks: Accepts
testing.TB(both*testing.Tand*testing.B)
WaitForMetricsServer
Wait for Prometheus metrics server to be ready:
func TestMetricsEndpoint(t *testing.T) {
t.Parallel()
recorder := metrics.TestingRecorderWithPrometheus(t, "test-service")
// Wait up to 5 seconds for server to start
err := metrics.WaitForMetricsServer(t, recorder.ServerAddress(), 5*time.Second)
if err != nil {
t.Fatalf("Metrics server not ready: %v", err)
}
// Server is ready, make requests (note: ServerAddress returns port like ":9090")
resp, err := http.Get("http://localhost" + recorder.ServerAddress() + "/metrics")
// ... test response
}
Signature
func WaitForMetricsServer(tb testing.TB, address string, timeout time.Duration) error
Parameters
tb testing.TB: Test or benchmark instance for loggingaddress string: Server address (e.g.,:9090)timeout time.Duration: Maximum wait time
Returns
error: Returns error if server doesn’t become ready within timeout
Testing Middleware
Test HTTP middleware with metrics collection:
func TestMiddleware(t *testing.T) {
t.Parallel()
recorder := metrics.TestingRecorder(t, "test-service")
// Create test handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
// Wrap with metrics middleware
wrappedHandler := metrics.Middleware(recorder)(handler)
// Make test request
req := httptest.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
wrappedHandler.ServeHTTP(w, req)
// Assert response
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
if w.Body.String() != "OK" {
t.Errorf("Expected body 'OK', got %s", w.Body.String())
}
// Metrics are recorded (visible in test logs if verbose)
}
Testing Custom Metrics
Test custom metric recording:
func TestCustomMetrics(t *testing.T) {
t.Parallel()
recorder := metrics.TestingRecorder(t, "test-service")
ctx := context.Background()
tests := []struct {
name string
record func() error
wantErr bool
}{
{
name: "valid counter",
record: func() error {
return recorder.IncrementCounter(ctx, "test_counter")
},
wantErr: false,
},
{
name: "invalid counter name",
record: func() error {
return recorder.IncrementCounter(ctx, "__reserved")
},
wantErr: true,
},
{
name: "valid histogram",
record: func() error {
return recorder.RecordHistogram(ctx, "test_duration", 1.5)
},
wantErr: false,
},
{
name: "valid gauge",
record: func() error {
return recorder.SetGauge(ctx, "test_gauge", 42)
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.record()
if (err != nil) != tt.wantErr {
t.Errorf("wantErr=%v, got err=%v", tt.wantErr, err)
}
})
}
}
Testing Error Handling
Test metric recording error handling:
func TestMetricErrors(t *testing.T) {
t.Parallel()
recorder := metrics.TestingRecorder(t, "test-service")
ctx := context.Background()
// Test invalid metric name
err := recorder.IncrementCounter(ctx, "http_invalid")
if err == nil {
t.Error("Expected error for reserved prefix, got nil")
}
// Test reserved prefix
err = recorder.IncrementCounter(ctx, "__internal")
if err == nil {
t.Error("Expected error for reserved prefix, got nil")
}
// Test valid metric
err = recorder.IncrementCounter(ctx, "valid_metric")
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
}
Integration Testing
Test complete HTTP server with metrics:
func TestServerWithMetrics(t *testing.T) {
recorder := metrics.TestingRecorderWithPrometheus(t, "test-api")
// Wait for metrics server
err := metrics.WaitForMetricsServer(t, recorder.ServerAddress(), 5*time.Second)
if err != nil {
t.Fatal(err)
}
// Create test HTTP server
mux := http.NewServeMux()
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "ok"}`))
})
handler := metrics.Middleware(recorder)(mux)
server := httptest.NewServer(handler)
defer server.Close()
// Make requests
resp, err := http.Get(server.URL + "/api")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}
// Check metrics endpoint (note: ServerAddress returns port like ":9090")
metricsResp, err := http.Get("http://localhost" + recorder.ServerAddress() + "/metrics")
if err != nil {
t.Fatal(err)
}
defer metricsResp.Body.Close()
body, _ := io.ReadAll(metricsResp.Body)
bodyStr := string(body)
// Verify metrics exist
if !strings.Contains(bodyStr, "http_requests_total") {
t.Error("Expected http_requests_total metric")
}
}
Parallel Tests
The testing utilities support parallel test execution:
func TestMetricsParallel(t *testing.T) {
tests := []struct {
name string
path string
}{
{"endpoint1", "/api/users"},
{"endpoint2", "/api/orders"},
{"endpoint3", "/api/products"},
}
for _, tt := range tests {
tt := tt // Capture range variable
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Each test gets its own recorder
recorder := metrics.TestingRecorder(t, "test-"+tt.name)
// Test handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrapped := metrics.Middleware(recorder)(handler)
req := httptest.NewRequest("GET", tt.path, nil)
w := httptest.NewRecorder()
wrapped.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected 200, got %d", w.Code)
}
})
}
}
Benchmarking
Benchmark metrics collection performance:
func BenchmarkMetricsMiddleware(b *testing.B) {
// Create recorder (use t=nil for benchmarks)
recorder, err := metrics.New(
metrics.WithStdout(),
metrics.WithServiceName("bench-service"),
)
if err != nil {
b.Fatal(err)
}
defer recorder.Shutdown(context.Background())
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrapped := metrics.Middleware(recorder)(handler)
req := httptest.NewRequest("GET", "/test", nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
wrapped.ServeHTTP(w, req)
}
}
func BenchmarkCustomMetrics(b *testing.B) {
recorder, err := metrics.New(
metrics.WithStdout(),
metrics.WithServiceName("bench-service"),
)
if err != nil {
b.Fatal(err)
}
defer recorder.Shutdown(context.Background())
ctx := context.Background()
b.Run("Counter", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = recorder.IncrementCounter(ctx, "bench_counter")
}
})
b.Run("Histogram", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = recorder.RecordHistogram(ctx, "bench_duration", 1.5)
}
})
b.Run("Gauge", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = recorder.SetGauge(ctx, "bench_gauge", 42)
}
})
}
Testing Best Practices
Use Parallel Tests
Enable parallel execution to run tests faster:
func TestSomething(t *testing.T) {
t.Parallel() // Always use t.Parallel() when safe
recorder := metrics.TestingRecorder(t, "test-service")
// ... test code
}
Prefer TestingRecorder
Use TestingRecorder (stdout) unless you specifically need to test the HTTP endpoint:
// Good - fast, no port allocation
recorder := metrics.TestingRecorder(t, "test-service")
// Only when needed - tests HTTP endpoint
recorder := metrics.TestingRecorderWithPrometheus(t, "test-service")
Wait for Server Ready
Always wait for Prometheus server before making requests:
recorder := metrics.TestingRecorderWithPrometheus(t, "test-service")
err := metrics.WaitForMetricsServer(t, recorder.ServerAddress(), 5*time.Second)
if err != nil {
t.Fatal(err)
}
// Now safe to make requests
Don’t Forget Context
Always pass context to metric methods:
ctx := context.Background()
err := recorder.IncrementCounter(ctx, "test_counter")
Test Error Cases
Test both success and error cases:
// Test valid metric
err := recorder.IncrementCounter(ctx, "valid_metric")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
// Test invalid metric
err = recorder.IncrementCounter(ctx, "__reserved")
if err == nil {
t.Error("Expected error for reserved prefix")
}
Example Test Suite
Complete example test suite:
package api_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"rivaas.dev/metrics"
"myapp/api"
)
func TestAPI(t *testing.T) {
t.Parallel()
recorder := metrics.TestingRecorder(t, "test-api")
server := api.NewServer(recorder)
tests := []struct {
name string
method string
path string
wantStatus int
}{
{"home", "GET", "/", 200},
{"users", "GET", "/api/users", 200},
{"not found", "GET", "/invalid", 404},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(tt.method, tt.path, nil)
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
if w.Code != tt.wantStatus {
t.Errorf("Expected status %d, got %d", tt.wantStatus, w.Code)
}
})
}
}
func TestMetricsEndpoint(t *testing.T) {
t.Parallel()
recorder := metrics.TestingRecorderWithPrometheus(t, "test-api")
err := metrics.WaitForMetricsServer(t, recorder.ServerAddress(), 5*time.Second)
if err != nil {
t.Fatal(err)
}
resp, err := http.Get("http://localhost" + recorder.ServerAddress() + "/metrics")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}
}
Next Steps
- See Examples for complete application examples
- Learn Custom Metrics to test your metrics
- Check Middleware for HTTP integration testing
- Review API Reference for method details
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.