Imported from WillAbides/oapitesthandler (
AGENTS.md). Install upstream withnpx skills add WillAbides/oapitesthandler. Copyright stays with the author.
AGENTS.md
This file provides guidance to AI Agents when working with code in this repository.
Overview
oapitesthandler is a code generator that creates test handlers for testing HTTP clients generated by oapi-codegen. It generates mock HTTP servers that implement the oapi-codegen strict server interface, allowing you to set expectations on API calls in your tests.
Development Commands
Testing
- Run all tests:
./script/test(orgo test -race -covermode=atomic ./...) - Run specific package tests:
go test ./path/to/package - Run single test:
go test -run TestName ./path/to/package
Snapshot Testing
The handlergen package uses snapshot testing to verify generated code output. Tests generate code to a temporary directory and compare it against reference snapshots stored in testdata/*/generated/ directories.
To regenerate snapshots, set the UPDATE_SNAPS environment variable when running tests:
- Regenerate all snapshots:
UPDATE_SNAPS=true go test ./internal/handlergen - Regenerate specific test snapshots:
UPDATE_SNAPS=true go test ./internal/handlergen -run TestRun/simple_get
The snapshots are stored in generated/ subdirectories within each test case's testdata directory (e.g., testdata/simple_get/generated/), making the reference code directly viewable in your IDE.
Code Quality
- Format code:
./script/fmt - Run linters:
./script/lint(or./bin/golangci-lint run ./...)
Go Style Guidelines
-
Never use
else: Prefer early returns and guard clauses overelseblocks. This reduces nesting and improves readability.- Good:
if err != nil { return err }followed by success path - Bad:
if err != nil { return err } else { /* success path */ }
- Good:
-
No assignments in if conditionals: Always separate variable assignments from conditional checks.
- Good:
val, ok := foo[bar]on one line, thenif ok {on the next - Bad:
if val, ok := foo[bar]; ok {
- Good:
Code Generation
- Generate all code:
./script/generate - The generate script runs
go generate ./...which triggers:- oapi-codegen to generate client/server code from OpenAPI specs
- oapitesthandler to generate test handler code
- Copying AGENTS.md to .github/copilot-instructions.md
- IMPORTANT: Always run
./script/generateafter updating AGENTS.md to verify that the documentation accurately reflects the current code generation behavior - IMPORTANT: Never edit
.github/copilot-instructions.mddirectly - it is generated from AGENTS.md when./script/generateis run. All documentation changes should be made to AGENTS.md.
Building
- Build and run:
./script/oapitesthandler [args] - Or use go tools:
go run github.com/willabides/oapitesthandler/cmd/oapitesthandler [args]
Usage Patterns
Shared Models with --models Flag
The --models flag allows you to use a shared package for OpenAPI model types instead of generating them in each test handler's output directory.
When to use --models:
- Sharing models between multiple test handlers for the same API
- Sharing models between test handlers and oapi-codegen client/server code
- Keeping test handler code separate from model definitions
- Working in a monorepo with shared API types
Example workflow:
// 1. Generate models in a shared package using oapi-codegen
//go:generate go tool oapi-codegen -config ./oapi-codegen.yaml ./openapi.yaml
// 2. Generate test handler with --models pointing to shared package
//go:generate go tool oapitesthandler ./openapi.yaml --config ./oapi-codegen.yaml --out internal/petstoretest --models ./internal/oapi
How it works:
- The shared package must already exist and contain the model types
- oapitesthandler reads the existing types and creates type aliases in the generated code
- Request/response objects reference the shared model types
- This ensures type compatibility across client, server, and test handler code
See example/petstore for a complete working example.
Architecture
Core Components
internal/handlergen/helpers/helpers.go - Core expectation matching library
expectResponses[REQ, RESP]type manages expected responses for requestsexpectResponse[REQ, RESP]type represents a single expected request/response pairkeyHash()creates deterministic hashes using JSON marshaling and FNV-128 hashingexpect()sets up expectations with optionalTimes()andMinTimes()options, acceptsrawRequestBody []bytefor matching io.Reader bodiesgetResponse()matches incoming requests to expectations, usesrawRequestBodyfor hash when provided (for io.Reader body matching)- For operations with generic Body (io.Reader), testServer reads the body bytes and passes to
getResponse()for accurate matching
internal/handlergen - Code generation engine
- Reads OpenAPI spec and oapi-codegen config YAML
- Generates files in output directory:
oapi_models_gen.go: OpenAPI model types (contains actual type definitions by default; when --models is used, contains type aliases referencing the external package)oapi_server_gen.go: Standard oapi-codegen output (strict server types, request/response objects)handler.go: TestHandler with builder types and Expect methods that return buildersserver.go: testServer that implements the strict server interface, reads io.Reader bodies for matchinghelpers.go: Helper types and functions (TB interface, ExpectOption, expectResponses, expectResponse)
- Supports optional --models flag to specify external package for OpenAPI models:
- When specified, custom oapi-codegen templates are injected via UserTemplates configuration
- Templates (typedef.tmpl, param-types.tmpl, constants.tmpl) generate type aliases instead of actual type definitions
- Templates reference types from modelspkg import (added via AdditionalImports)
- Allows sharing model types between multiple test handlers or with client/server code
- Uses templates to generate handler methods:
- Operations without bodies: path/query parameters extracted as individual arguments
- Operations with bodies: multiple methods using oapi-codegen's
Suffix()for naming - WithBody methods accept
[]byteand pass asrawRequestBodytoexpect()
- testServer methods conditionally read req.Body and pass bytes to
getResponse()for operations with generic bodies
cmd/oapitesthandler - CLI entry point
- Uses kong for CLI parsing
- Takes: OpenAPI spec, oapi-codegen config, output directory, optional models package
- Optional --models flag specifies Go import path for external models package (e.g.,
./internal/oapiorgithub.com/user/pkg/models) - Delegates to handlergen.Run()
Generated Code Pattern
For each OpenAPI operation, the generator creates:
- An
expectResponsesfield in TestHandler (e.g.,getPetByIdExpectResponses) - An
Expect{OperationID}method on TestHandler that returns an{OperationID}Expectationbuilder - Builder methods for each response type (e.g.,
RespondJSON200,Respond404) - A handler method on testServer that calls
getResponse()on the expectResponses field
The generator uses a fluent builder API pattern where Expect methods return a builder, and you chain a Respond method to set the response:
Operations WITHOUT request bodies (GET, DELETE without body) have expanded parameters:
// Path parameters only
handler.ExpectGetPetById(petId int64, opts...).RespondJSON200(response)
// Query parameters only
handler.ExpectFindPetsByStatus(queryParams *FindPetsByStatusParams, opts...).RespondJSON200(response)
// Path + query parameters
handler.ExpectDeletePet(petId int64, queryParams *DeletePetParams, opts...).Respond200()
// No parameters at all
handler.ExpectGetStoreInventory(opts...).RespondJSON200(response)
Operations WITH request bodies (POST, PUT, PATCH) generate multiple methods, one per content type:
// Default method for JSON (most common)
handler.ExpectUpdateUser(username string, body UpdateUserJSONRequestBody, opts...).RespondJSON200(response)
// Method for formdata content type
handler.ExpectUpdateUserWithFormdataBody(username string, body UpdateUserFormdataRequestBody, opts...).RespondJSON200(response)
// Generic method for arbitrary content types
handler.ExpectUpdateUserWithBody(username string, contentType string, body []byte, opts...).RespondJSON200(response)
Response methods are generated based on the OpenAPI responses:
// JSON responses include content type in method name
builder.RespondJSON200(response) // 200 OK with JSON content
builder.RespondJSON404(response) // 404 Not Found with JSON content
// Responses without content (like 204 No Content, or empty 200 responses)
builder.Respond204() // No parameter - response has no content
builder.Respond200() // No parameter - response has no content
// Custom handler with full HTTP control
builder.Handle(func(req RequestObject, w http.ResponseWriter) {
// Full control over HTTP response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
json.NewEncoder(w).Encode(dynamicResponse)
})
The method naming uses oapi-codegen's Suffix() method:
- Default body type (usually JSON): no suffix
- Named body types:
With{NameTag}Bodysuffix (e.g.,WithFormdataBody) - Generic body:
WithBodysuffix, acceptscontentTypeand[]byte - Response methods:
Respond{ContentType}{StatusCode}(e.g.,RespondJSON200) - Responses without content:
Respond{StatusCode}(e.g.,Respond204)
Example workflow for operation without body:
// In test:
handler.ExpectGetPetById(1).RespondJSON200(responseObj)
// Under the hood:
// 1. ExpectGetPetById constructs GetPetByIdRequestObject{PetId: 1} and returns builder
// 2. RespondJSON200 stores expectation with expect(req, nil, resp, opts)
// 3. HTTP request triggers StrictHandler
// 4. StrictHandler calls testServer.GetPetById()
// 5. testServer.GetPetById calls getResponse(req, nil)
// 6. Response returned if expectation matches
Example workflow for operation with generic body:
// In test:
bodyBytes := []byte(`<User><id>1</id><username>john</username></User>`)
handler.ExpectUpdateUserWithBody("john", "application/xml", bodyBytes).RespondJSON200(responseObj)
// Under the hood:
// 1. ExpectUpdateUserWithBody constructs UpdateUserRequestObject{Username: "john", Body: bytes.NewReader(bodyBytes)}
// 2. Returns builder with rawBody=bodyBytes
// 3. RespondJSON200 stores expectation with expect(req, bodyBytes, resp, opts) - rawRequestBody=bodyBytes
// 4. HTTP request triggers StrictHandler
// 5. StrictHandler calls testServer.UpdateUser()
// 6. testServer reads req.Body using io.ReadAll to get rawBytes
// 7. testServer calls getResponse(req, rawBytes)
// 8. Response returned if expectation matches (using rawBytes for hash)
Example with options:
// Expect the same call 3 times with Times option
handler.ExpectGetPetById(1, petstoretest.Times(3)).RespondJSON200(responseObj)
// Different response codes for different scenarios
handler.ExpectGetPetById(1).RespondJSON200(foundResponse)
handler.ExpectGetPetById(999).Respond404()
// Custom handler with dynamic responses
handler.ExpectGetPetById(1).Handle(func(req GetPetByIdRequestObject, w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
json.NewEncoder(w).Encode(map[string]any{
"id": req.PetId,
"name": fmt.Sprintf("Pet-%d", req.PetId),
})
})
Testing Pattern
Tests use httptest.NewServer with the TestHandler:
- Create TestHandler with NewTestHandler(t)
- Start httptest.NewServer with the handler
- Create oapi-codegen client pointing to test server
- Set expectations on the handler
- Make client calls - they hit the mock server
- Cleanup verifies all expectations were met
Key Design Decisions
- Fluent builder API pattern: Expect methods return a builder with type-safe Respond methods for each possible response type, providing better discoverability through IDE autocomplete
- Response method generation: For each response defined in the OpenAPI spec, a
Respond{ContentType}{StatusCode}method is generated on the builder (non-integer status codes like "default" are skipped) - Empty response handling: Responses without content (no schema defined) generate parameterless methods like
Respond200()instead of requiring an empty struct parameter - Options placement: Options like
Times()are passed to the Expect method, not the Respond method, for cleaner syntax:ExpectGetPetById(1, Times(3)).RespondJSON200(...) - Custom handlers via Handle() method: Each builder generates a
Handle()method that accepts a function with signaturefunc(RequestObject, http.ResponseWriter), providing full control over HTTP responses for dynamic testing scenarios- Uses raw responder pattern: generates a
{operationId}RawRespondertype that implements the Visit interface - Handler receives both the typed request object AND raw
http.ResponseWriter - Integrates seamlessly with existing
expect()mechanism - no changes to helpers.go needed - Works with all options including
Times() - Particularly useful for stateful responses, conditional logic, or custom headers
- Uses raw responder pattern: generates a
- Request matching with rawRequestBody: For operations with generic io.Reader bodies, the raw bytes are passed separately to
expect()andgetResponse()for matching, since io.Reader fields are consumed and can't be reliably hashed - Ergonomic method signatures: Path and query parameters are extracted as individual function parameters instead of requiring full RequestObject construction
- Multiple methods per operation: Operations with request bodies generate separate methods for each content type (JSON, formdata, generic), all returning the same builder type
- Method naming: Uses oapi-codegen's
Suffix()method - default body gets no suffix, others getWith{NameTag}Body - Generic body accepts []byte: WithBody methods accept
[]byteinstead ofio.Readerfor convenience, converting internally withbytes.NewReader() - FIFO expectation matching: First matching expectation with remaining times is used
- Expectation tracking: Each expectation tracks remaining invocations via
timescounter - Automatic verification: Cleanup functions verify all expectations were fully consumed
- Type safety: Generated code uses oapi-codegen's strict server interface
oapi-codegen Integration
This tool requires an oapi-codegen config YAML that specifies:
package: Package name for generated code- Optional
import-mappingfor external references
The example/petstore demonstrates this pattern with //go:generate directives.
