Skip to content

Testing

λ Cosmos includes testing utilities in both the router and framework modules. Since everything builds on Go’s standard net/http and net/http/httptest packages, you can use familiar patterns.

The Handler.Record method executes a handler with a test request and returns the response:

import (
"net/http"
"net/http/httptest"
"testing"
"github.com/studiolambda/cosmos/framework"
"github.com/studiolambda/cosmos/contract/response"
)
func myHandler(w http.ResponseWriter, r *http.Request) error {
return response.JSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func TestMyHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
res := framework.Handler(myHandler).Record(req)
if res.StatusCode != http.StatusOK {
t.Errorf("expected 200, got %d", res.StatusCode)
}
}

Record runs the full handler lifecycle including hooks, error handling, and the 204 No Content fallback. Use httptest.NewRequestWithContext(t.Context(), ...) to pass a test context for proper cleanup.

func notFoundHandler(w http.ResponseWriter, r *http.Request) error {
return problem.Problem{
Title: "Not Found",
Status: http.StatusNotFound,
}
}
func TestNotFound(t *testing.T) {
req := httptest.NewRequest("GET", "/missing", nil)
req.Header.Set("Accept", "application/json")
res := framework.Handler(notFoundHandler).Record(req)
if res.StatusCode != http.StatusNotFound {
t.Errorf("expected 404, got %d", res.StatusCode)
}
}

Wrap your handler in middleware before recording:

func TestWithMiddleware(t *testing.T) {
handler := middleware.Recover()(myHandler)
req := httptest.NewRequest("GET", "/", nil)
res := framework.Handler(handler).Record(req)
// ...
}

The router module provides several testing helpers.

Check if a route pattern is registered:

func TestRouteRegistration(t *testing.T) {
app := framework.New()
app.Get("/users", listUsers)
app.Get("/users/{id}", getUser)
if !app.Has("GET /users") {
t.Error("expected /users route to be registered")
}
if !app.Has("GET /users/{id}") {
t.Error("expected /users/{id} route to be registered")
}
}

Check if a URL path matches a registered route:

func TestRouteMatching(t *testing.T) {
app := framework.New()
app.Get("/users/{id}", getUser)
if !app.Matches("GET", "/users/42") {
t.Error("expected /users/42 to match")
}
if app.Matches("POST", "/users/42") {
t.Error("expected POST /users/42 not to match")
}
}

Retrieve the handler for a registered pattern:

handler, ok := app.Handler("GET /users/{id}")
if !ok {
t.Fatal("expected handler to be registered")
}

Retrieve the handler that matches a given method and URL:

handler, pattern, ok := app.HandlerMatch("GET", "/users/42")
if !ok {
t.Fatal("expected a handler to match")
}
// pattern == "GET /users/{id}"

Execute a test request against the router (not just a single handler):

func TestFullStack(t *testing.T) {
app := framework.New()
app.Use(middleware.Recover())
app.Get("/hello", func(w http.ResponseWriter, r *http.Request) error {
return response.String(w, http.StatusOK, "Hello, World!")
})
req := httptest.NewRequest("GET", "/hello", nil)
res := app.Record(req)
if res.StatusCode != http.StatusOK {
t.Errorf("expected 200, got %d", res.StatusCode)
}
}

Router.Record runs the full request through the router’s ServeHTTP, including all middleware, route matching, and error handling.

For full integration tests that include an actual HTTP server:

func TestIntegration(t *testing.T) {
app := framework.New()
app.Get("/health", func(w http.ResponseWriter, r *http.Request) error {
return response.JSON(w, http.StatusOK, map[string]string{"status": "healthy"})
})
server := httptest.NewServer(app)
defer server.Close()
resp, err := http.Get(server.URL + "/health")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected 200, got %d", resp.StatusCode)
}
}

The contract/mock package provides generated mocks for all contract interfaces (Cache, Database, Session, etc.). These are useful for unit testing handlers that depend on external services:

// Use the mock implementations from contract/mock
// to test handlers in isolation without external dependencies.

Since all service dependencies are accessed through interfaces (contract.Cache, contract.Database, etc.), you can easily substitute mocks or test doubles using the Provide middleware or by constructing handlers directly with test implementations.