Skip to Content
Steel is in alpha 🎉
Testing

Testing

SteelRouter includes comprehensive testing utilities that make it easy to test your APIs with minimal boilerplate. From simple unit tests to complex integration tests and load testing, SteelRouter has you covered.

Quick Start

Here’s a simple test using SteelRouter’s testing utilities:

func TestUserAPI(t *testing.T) { // Set up test router router := router.NewTestRouter(). WithRoute("GET", "/users/:id", getUserHandler). WithRoute("POST", "/users", createUserHandler). Build() // Test getting a user response := router.NewRequest("GET", "/users/123"). Execute(router) router.AssertResponse(t, response). Status(http.StatusOK). IsJSON(). JSON("id", float64(123)). JSON("name", "John Doe") }

Test Router Builder

The TestRouterBuilder helps you create routers for testing with common middleware and configurations:

Basic Setup

func setupTestRouter() *router.SteelRouter { return router.NewTestRouter(). WithMiddleware(router.Logger). WithRecovery(). WithCORS(). WithAuth("test-token"). WithRoute("GET", "/health", router.MockHandler(http.StatusOK, "OK")). Build() }

Builder Options

// Add middleware router.NewTestRouter(). WithMiddleware(router.Logger, router.Recoverer). WithLogging(). WithRecovery(). WithTimeout(30 * time.Second) // Add authentication router.NewTestRouter(). WithAuth("valid-token") // Simple bearer token auth // Add CORS support router.NewTestRouter(). WithCORS() // Add routes router.NewTestRouter(). WithRoute("GET", "/users", handler). WithOpinionatedRoute("POST", "/users", opinionatedHandler). WithOpenAPI()

Complete Example

func TestCompleteAPI(t *testing.T) { // Mock database userDB := &MockUserDB{ users: []User{ {ID: 1, Name: "Alice", Email: "alice@example.com"}, {ID: 2, Name: "Bob", Email: "bob@example.com"}, }, } // Build test router router := router.NewTestRouter(). WithAuth("test-token"). WithRecovery(). WithOpinionatedRoute("GET", "/users/:id", func(ctx *router.Context, req GetUserRequest) (*User, error) { user, exists := userDB.GetUser(req.ID) if !exists { return nil, router.NotFound("User") } return user, nil }). WithOpinionatedRoute("POST", "/users", func(ctx *router.Context, req CreateUserRequest) (*User, error) { user := userDB.CreateUser(req.Name, req.Email) return user, nil }). Build() // Test cases testCases := []struct { name string test func(t *testing.T) }{ {"Get existing user", testGetExistingUser}, {"Get non-existent user", testGetNonExistentUser}, {"Create new user", testCreateUser}, {"Unauthorized access", testUnauthorized}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tc.test(t) }) } }

Request Builder

The RequestBuilder provides a fluent API for creating test requests:

Basic Requests

// Simple GET request response := router.NewRequest("GET", "/users/123"). Execute(router) // POST with JSON body response := router.NewRequest("POST", "/users"). WithJSON(map[string]interface{}{ "name": "John Doe", "email": "john@example.com", "age": 30, }). Execute(router) // Request with headers response := router.NewRequest("GET", "/protected"). WithHeader("Authorization", "Bearer token123"). WithHeader("X-Request-ID", "test-123"). Execute(router)

Query Parameters

response := router.NewRequest("GET", "/users"). WithQuery("page", "1"). WithQuery("limit", "10"). WithQuery("search", "john"). Execute(router) // URL: /users?page=1&limit=10&search=john

Authentication

// Bearer token response := router.NewRequest("GET", "/protected"). WithAuth("your-token"). Execute(router) // Custom header response := router.NewRequest("GET", "/api"). WithHeader("X-API-Key", "api-key-123"). Execute(router)

Different Body Types

// JSON body response := router.NewRequest("POST", "/users"). WithJSON(user). Execute(router) // String body response := router.NewRequest("POST", "/webhook"). WithBody("raw webhook payload"). Execute(router) // Byte body response := router.NewRequest("POST", "/upload"). WithBody([]byte("binary data")). Execute(router)

Response Assertions

The ResponseAssertion provides fluent assertions for testing responses:

Status Code Assertions

router.AssertResponse(t, response). Status(http.StatusOK) router.AssertResponse(t, response). Status(http.StatusCreated) router.AssertResponse(t, response). Status(http.StatusNotFound)

Body Assertions

// Exact body match router.AssertResponse(t, response). Body("Expected response body") // Body contains router.AssertResponse(t, response). BodyContains("partial text") // JSON validation router.AssertResponse(t, response). IsJSON()

JSON Field Assertions

router.AssertResponse(t, response). IsJSON(). JSON("id", float64(123)). // JSON numbers are float64 JSON("name", "John Doe"). JSON("active", true). JSONExists("created_at"). JSONExists("updated_at") // Nested JSON fields router.AssertResponse(t, response). JSON("user.profile.name", "John"). JSON("settings.theme", "dark")

Header Assertions

router.AssertResponse(t, response). Header("Content-Type", "application/json"). HeaderExists("X-Request-ID"). Header("Location", "/users/123")

Chaining Assertions

router.AssertResponse(t, response). Status(http.StatusCreated). Header("Content-Type", "application/json"). Header("Location", "/users/123"). IsJSON(). JSON("id", float64(123)). JSON("name", "John Doe"). JSONExists("created_at")

Mock Handlers

SteelRouter provides utilities for creating mock handlers:

Simple Mocks

// Simple response handler := router.MockHandler(http.StatusOK, "Hello World") // JSON response handler := router.MockJSONHandler(http.StatusOK, map[string]interface{}{ "message": "Success", "data": []string{"item1", "item2"}, }) // Error response handler := router.MockErrorHandler(router.BadRequest("Invalid input"))

Dynamic Mocks

func mockUserHandler(t *testing.T) router.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID := router.URLParam(r, "id") switch userID { case "1": router.MockJSONHandler(http.StatusOK, User{ ID: 1, Name: "Alice", })(w, r) case "999": router.MockErrorHandler(router.NotFound("User"))(w, r) default: router.MockJSONHandler(http.StatusOK, User{ ID: 2, Name: "Default User", })(w, r) } } }

Integration Testing

Complete API Testing

func TestUserAPIIntegration(t *testing.T) { // Set up test database db := setupTestDB(t) defer db.Close() // Create router with real handlers r := router.NewTestRouter(). WithAuth("test-token"). WithOpinionatedRoute("GET", "/users/:id", func(ctx *router.Context, req GetUserRequest) (*User, error) { return getUserFromDB(db, req.ID) }). WithOpinionatedRoute("POST", "/users", func(ctx *router.Context, req CreateUserRequest) (*User, error) { return createUserInDB(db, req) }). WithOpinionatedRoute("PUT", "/users/:id", func(ctx *router.Context, req UpdateUserRequest) (*User, error) { return updateUserInDB(db, req) }). WithOpinionatedRoute("DELETE", "/users/:id", func(ctx *router.Context, req DeleteUserRequest) (*router.APIResponse, error) { err := deleteUserFromDB(db, req.ID) if err != nil { return nil, err } return router.NoContent(), nil }). Build() t.Run("CRUD Operations", func(t *testing.T) { // Create user createResp := router.NewRequest("POST", "/users"). WithAuth("test-token"). WithJSON(map[string]interface{}{ "name": "Integration Test User", "email": "test@example.com", "age": 25, }). Execute(r) router.AssertResponse(t, createResp). Status(http.StatusOK). IsJSON(). JSON("name", "Integration Test User"). JSONExists("id") // Extract created user ID userID := int(createResp.JSON["id"].(float64)) // Get user getResp := router.NewRequest("GET", fmt.Sprintf("/users/%d", userID)). WithAuth("test-token"). Execute(r) router.AssertResponse(t, getResp). Status(http.StatusOK). JSON("id", float64(userID)). JSON("name", "Integration Test User") // Update user updateResp := router.NewRequest("PUT", fmt.Sprintf("/users/%d", userID)). WithAuth("test-token"). WithJSON(map[string]interface{}{ "name": "Updated User", "email": "updated@example.com", "age": 26, }). Execute(r) router.AssertResponse(t, updateResp). Status(http.StatusOK). JSON("name", "Updated User") // Delete user deleteResp := router.NewRequest("DELETE", fmt.Sprintf("/users/%d", userID)). WithAuth("test-token"). Execute(r) router.AssertResponse(t, deleteResp). Status(http.StatusNoContent) // Verify deletion getDeletedResp := router.NewRequest("GET", fmt.Sprintf("/users/%d", userID)). WithAuth("test-token"). Execute(r) router.AssertResponse(t, getDeletedResp). Status(http.StatusNotFound) }) }

Middleware Testing

func TestAuthMiddleware(t *testing.T) { r := router.NewTestRouter(). WithAuth("valid-token"). WithRoute("GET", "/protected", router.MockHandler(http.StatusOK, "Protected")). Build() testCases := []struct { name string token string statusCode int }{ {"Valid token", "valid-token", http.StatusOK}, {"Invalid token", "invalid-token", http.StatusUnauthorized}, {"No token", "", http.StatusUnauthorized}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { req := router.NewRequest("GET", "/protected") if tc.token != "" { req = req.WithAuth(tc.token) } response := req.Execute(r) router.AssertResponse(t, response). Status(tc.statusCode) }) } }

Load Testing

SteelRouter includes built-in load testing capabilities:

Basic Load Test

func TestAPIPerformance(t *testing.T) { r := setupTestRouter() config := router.LoadTestConfig{ Concurrency: 10, // 10 concurrent users Requests: 1000, // 1000 total requests Timeout: 30 * time.Second, Paths: []string{"/users", "/health"}, } result := router.RunLoadTest(r, config) // Assert performance metrics router.AssertTrue(t, result.SuccessRequests > 950) // 95% success rate router.AssertTrue(t, result.RequestsPerSecond > 100) // At least 100 RPS router.AssertTrue(t, result.AverageLatency < 100*time.Millisecond) // Under 100ms average t.Logf("Performance Results:") t.Logf(" Requests per second: %.2f", result.RequestsPerSecond) t.Logf(" Average latency: %v", result.AverageLatency) t.Logf(" Success rate: %.2f%%", float64(result.SuccessRequests*100)/float64(result.TotalRequests)) }

Custom Load Test

func TestUserAPILoad(t *testing.T) { r := setupUserAPI() // Test different endpoints with different loads endpoints := []struct { path string concurrency int requests int }{ {"/users", 5, 500}, {"/users/1", 10, 1000}, {"/health", 20, 2000}, } for _, endpoint := range endpoints { t.Run(endpoint.path, func(t *testing.T) { config := router.LoadTestConfig{ Concurrency: endpoint.concurrency, Requests: endpoint.requests, Timeout: 30 * time.Second, Paths: []string{endpoint.path}, } result := router.RunLoadTest(r, config) if result.FailedRequests > 0 { t.Errorf("Load test failed: %d failed requests", result.FailedRequests) } t.Logf("%s - RPS: %.2f, Avg Latency: %v", endpoint.path, result.RequestsPerSecond, result.AverageLatency) }) } }

Benchmark Testing

Use Go’s built-in benchmarking with SteelRouter’s utilities:

Basic Benchmarks

func BenchmarkUserAPI(b *testing.B) { r := setupTestRouter() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { response := router.NewRequest("GET", "/users/1"). Execute(r) if response.StatusCode != http.StatusOK { b.Errorf("Expected 200, got %d", response.StatusCode) } } }) }

Benchmark Setup Helper

func BenchmarkRouterPerformance(b *testing.B) { setup := router.NewBenchmarkSetup(). AddStaticRoutes(100). // Add 100 static routes AddParameterRoutes(50). // Add 50 parameter routes AddRoute("GET", "/complex/:id/nested/:nested", router.MockHandler(http.StatusOK, "OK")) r := setup.Setup() paths := []string{ "/route1", "/route50", "/route99", "/param1/123", "/param25/456", "/param49/789", "/complex/123/nested/456", } b.ResetTimer() for i := 0; i < b.N; i++ { path := paths[i%len(paths)] req, _ := router.NewRequest("GET", path).Build() w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { b.Errorf("Expected 200, got %d for path %s", w.Code, path) } } }

Test Utilities

Helper Functions

// Assert functions router.AssertEqual(t, expected, actual) router.AssertNotEqual(t, expected, actual) router.AssertTrue(t, condition) router.AssertFalse(t, condition) router.AssertContains(t, haystack, needle) router.AssertNotContains(t, haystack, needle) router.AssertNoError(t, err) router.AssertError(t, err) router.AssertPanic(t, func() { /* code that should panic */ }) // JSON utilities data := router.MustMarshalJSON(map[string]string{"key": "value"}) router.MustUnmarshalJSON(data, &result) // Test data generators users := router.CreateTestUsers(10) // Creates 10 test users

Table-Driven Tests

func TestUserValidation(t *testing.T) { r := setupTestRouter() testCases := []struct { name string user map[string]interface{} expectedCode int expectedError string }{ { name: "Valid user", user: map[string]interface{}{ "name": "John Doe", "email": "john@example.com", "age": 25, }, expectedCode: http.StatusOK, }, { name: "Missing name", user: map[string]interface{}{ "email": "john@example.com", "age": 25, }, expectedCode: http.StatusBadRequest, expectedError: "name is required", }, { name: "Invalid email", user: map[string]interface{}{ "name": "John Doe", "email": "invalid-email", "age": 25, }, expectedCode: http.StatusBadRequest, expectedError: "invalid email format", }, { name: "Age too young", user: map[string]interface{}{ "name": "John Doe", "email": "john@example.com", "age": 17, }, expectedCode: http.StatusBadRequest, expectedError: "must be 18 or older", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { response := router.NewRequest("POST", "/users"). WithAuth("test-token"). WithJSON(tc.user). Execute(r) assertion := router.AssertResponse(t, response). Status(tc.expectedCode) if tc.expectedError != "" { assertion.BodyContains(tc.expectedError) } }) } }

WebSocket and SSE Testing

WebSocket Testing

func TestWebSocketHandler(t *testing.T) { r := router.NewRouter() // Register WebSocket handler r.WebSocket("/ws/test", func(conn *router.WSConnection, message struct { Text string `json:"text"` }) (*struct { Echo string `json:"echo"` }, error) { return &struct { Echo string `json:"echo"` }{ Echo: "Echo: " + message.Text, }, nil }) // Test handler registration router.AssertEqual(t, 1, len(r.wsHandlers)) handler, exists := r.wsHandlers["/ws/test"] router.AssertTrue(t, exists) router.AssertEqual(t, "/ws/test", handler.Path) }

SSE Testing

func TestSSEHandler(t *testing.T) { r := router.NewRouter() // Register SSE handler r.SSE("/sse/events", func(conn *router.SSEConnection, params struct{}) error { return conn.SendMessage(router.SSEMessage{ Event: "test", Data: "test data", }) }) // Test handler registration router.AssertEqual(t, 1, len(r.sseHandlers)) handler, exists := r.sseHandlers["/sse/events"] router.AssertTrue(t, exists) router.AssertEqual(t, "/sse/events", handler.Path) }

Best Practices

Test Organization: Organize your tests by feature or endpoint, use table-driven tests for multiple scenarios, and separate unit tests from integration tests.

1. Test Structure

func TestUserAPI(t *testing.T) { // Setup router := setupTestRouter(t) t.Run("Authentication", func(t *testing.T) { // Auth-related tests }) t.Run("CRUD Operations", func(t *testing.T) { // CRUD tests }) t.Run("Validation", func(t *testing.T) { // Validation tests }) t.Run("Error Handling", func(t *testing.T) { // Error tests }) }

2. Test Data Management

func setupTestData(t *testing.T) *TestData { return &TestData{ Users: []User{ {ID: 1, Name: "Alice", Email: "alice@test.com"}, {ID: 2, Name: "Bob", Email: "bob@test.com"}, }, ValidToken: "test-token-123", } } func (td *TestData) Cleanup() { // Clean up test data }

3. Mock Services

type MockUserService struct { users map[int]User } func (m *MockUserService) GetUser(id int) (*User, error) { user, exists := m.users[id] if !exists { return nil, router.NotFound("User") } return &user, nil } func setupMockServices() *MockUserService { return &MockUserService{ users: map[int]User{ 1: {ID: 1, Name: "Test User"}, }, } }

4. Environment-Specific Tests

func TestIntegration(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } if os.Getenv("INTEGRATION_TESTS") != "true" { t.Skip("Skipping integration test (set INTEGRATION_TESTS=true)") } // Integration test code... }

SteelRouter’s testing utilities make it easy to write comprehensive tests that give you confidence in your API’s reliability and performance.

Last updated on