Skip to main content

Testing

Okapi provides comprehensive testing utilities through the okapitest package to help you write robust tests for your handlers and middleware.

Quick Start​

import (
"testing"

"github.com/jkaninda/okapi"
"github.com/jkaninda/okapi/okapitest"
)

func TestGetBooksHandler(t *testing.T) {
// Create a test server
server := okapi.NewTestServer(t)
server.Get("/books", GetBooksHandler)

// Make request and assert response
okapitest.GET(t, server.BaseURL+"/books").
ExpectStatusOK().
ExpectBodyContains("The Go Programming Language")
}

Test Servers​

ConstructorDescription
okapi.NewTestServer(t)An okapi.New() instance behind an httptest.Server, closed on cleanup
okapi.DefaultTestServer(t)The same, from okapi.Default() — documentation routes included
okapi.NewTestServerWithOkapi(t, o)Wraps an instance you configured yourself
okapi.NewTestServerOn(t, port)Starts a real listener on port (for tests that need a fixed address)

All of them return a *okapi.TestServer, which embeds *okapi.Okapi — so you register routes and middleware on it exactly as on an application — and exposes BaseURL.

For a handler test that needs no server at all, okapi.NewTestContext(method, url, body) returns a *okapi.Context and the *httptest.ResponseRecorder behind it.

Testing Approaches​

The test client provides a fluent API for making multiple requests with shared configuration:

func TestBooksAPI(t *testing.T) {
// Setup test server
server := okapi.NewTestServer(t)
server.Get("/books", GetBooksHandler)
server.Get("/books/:id", GetBookHandler)
server.Post("/books", CreateBookHandler)

// Create reusable client
client := okapitest.NewClient(t, server.BaseURL)

// Test listing books
client.GET("/books").
ExpectStatusOK().
ExpectBodyContains("The Go Programming Language").
ExpectHeader("X-Version", "1.0.0")

// Test getting a specific book
client.GET("/books/1").
ExpectStatusOK().
ExpectBodyContains("The Go Programming Language")

// Test book not found
client.GET("/books/999").
ExpectStatusNotFound().
ExpectBodyContains("Book not found")

// Test creating a book
newBook := Book{
ID: 6,
Name: "Sample Book",
Price: 20,
Year: 2024,
Qty: 5,
}
client.POST("/books").
JSONBody(newBook).
ExpectStatusCreated().
ExpectBodyContains("Sample Book")
}

Headers set on client.Headers are merged into every request the client builds.

2. Using Standalone Request Helpers​

For simpler test cases or one-off requests:

func TestGetBookHandler(t *testing.T) {
server := okapi.NewTestServer(t)
server.Get("/books/:id", GetBookHandler)

// Test successful retrieval
okapitest.GET(t, server.BaseURL+"/books/1").
ExpectStatusOK().
ExpectBodyContains("The Go Programming Language")

// Test not found scenario
okapitest.GET(t, server.BaseURL+"/books/999").
ExpectStatusNotFound().
ExpectBodyContains("Book not found")
}

func TestCreateBookHandler(t *testing.T) {
server := okapi.NewTestServer(t)
server.Post("/books", CreateBookHandler)

book := Book{
ID: 6,
Name: "Sample Book",
Price: 20,
Year: 2024,
Qty: 5,
}

okapitest.POST(t, server.BaseURL+"/books").
JSONBody(book).
ExpectStatusCreated().
ExpectBodyContains("Sample Book")
}

GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS all take (t, url). okapitest.Request(t) returns a bare builder when you want to set the method and URL yourself, and okapitest.FromRecorder(t, rec) asserts against a response you already recorded.

Available Assertions​

// Status code assertions
.ExpectStatus(code int) // Custom status code
.ExpectStatusOK() // 200
.ExpectStatusCreated() // 201
.ExpectStatusAccepted() // 202
.ExpectStatusNoContent() // 204
.ExpectStatusBadRequest() // 400
.ExpectStatusUnauthorized() // 401
.ExpectStatusForbidden() // 403
.ExpectStatusNotFound() // 404
.ExpectStatusConflict() // 409
.ExpectStatusInternalServerError() // 500

// Body assertions
.ExpectBody(expected string) // exact body match
.ExpectBodyContains(substr string)
.ExpectBodyNotContains(substr string)
.ExpectEmptyBody()
.ExpectJSON(expected any) // compares decoded JSON
.ExpectJSONPath(path string, expected any) // dot-path lookup, e.g. "data.name"

// Header, cookie and content-type assertions
.ExpectHeader(key, value string)
.ExpectHeaderContains(key, substr string)
.ExpectHeaderExists(key string)
.ExpectCookie(key, value string)
.ExpectCookieExist(key string)
.ExpectContentType(contentType string)

ExpectBody is the exact-match assertion (there is no ExpectBodyEquals), and ExpectJSON is the JSON comparison (there is no ExpectJSONBody).

To inspect the response yourself, use .ParseJSON(&target) inside the chain, or .Execute() to get the *http.Response and the body bytes.

Request Building​

okapitest.POST(t, url).
Header("X-Trace-Id", "abc").
Headers(map[string]string{"X-A": "1", "X-B": "2"}).
QueryParam("dry_run", "true").
QueryParams(map[string]string{"page": "1"}).
SetBearerAuth("token123").
SetBasicAuth("user", "pass").
JSONBody(payload). // or FormBody(map[string]string), Body(io.Reader)
Timeout(5 * time.Second).
ExpectStatusCreated()

Testing with Custom Headers​

func TestAuthenticatedRequest(t *testing.T) {
server := okapi.NewTestServer(t)
server.Get("/protected", ProtectedHandler)

client := okapitest.NewClient(t, server.BaseURL)

client.GET("/protected").
Header("Authorization", "Bearer token123").
ExpectStatusOK()
}

Testing Middleware​

func TestAuthMiddleware(t *testing.T) {
server := okapi.NewTestServer(t)
server.Use(AuthMiddleware)
server.Get("/protected", ProtectedHandler)

client := okapitest.NewClient(t, server.BaseURL)

// Test without auth - should fail
client.GET("/protected").
ExpectStatus(401)

// Test with auth - should succeed
client.GET("/protected").
Header("Authorization", "Bearer valid-token").
ExpectStatusOK()
}

Testing a Server That Runs Until Signalled​

okapitest.GracefulExitAfter(d) sends the current process SIGTERM after d, which is how you exercise a server that only stops on a signal — such as one started with okapicli.RunServer:

func TestRunServer(t *testing.T) {
okapitest.GracefulExitAfter(500 * time.Millisecond)

if err := cli.RunServer(); err != nil {
t.Fatal(err)
}
}

The helper compiles on Windows, where a process cannot signal itself, but panics with an explanation if it is called there.