Documentation
¶
Overview ¶
bootstrap.go provides integration test bootstrap helpers.
These helpers reduce setup boilerplate for package-level integration test suites without hiding behavior. They complement the unit-test helpers in the rest of the package.
All functions are TEST-ONLY. They must not be imported by production code.
Usage:
func TestMain(m *testing.M) {
testkit.MainWithEnv(m,
testkit.RequireEnv("DATABASE_URL"),
testkit.RequireEnv("QUEUE_URL"),
)
}
func TestSomething(t *testing.T) {
testkit.SkipIfShort(t)
// integration test body
}
Package testkit provides reusable test helpers that reduce setup cost without hiding behavior.
The package is helper-oriented, not a custom test framework. Helpers are composable and explicit. This file provides fake clock primitives for deterministic time-dependent testing.
testkit must not lock projects into custom assertion semantics. Standard library testing patterns remain valid alongside these helpers.
Usage:
fc := testkit.NewFakeClock(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)) // Pass fc.Now as a time source to the unit under test. svc := myservice.New(myservice.WithClock(fc)) fc.Advance(5 * time.Minute) // Now the service sees time 2026-01-01 00:05:00 UTC.
dbfixture.go provides DB fixture helpers and cleanup utilities for tests.
These helpers are TEST-ONLY. They must not be imported by production code. All functions require a *testing.T to enforce test-only usage.
DB connections in tests must be isolated: each test should either use a transaction that is rolled back on cleanup, or a short-lived database created for the test run.
Package testkit provides reusable, composable test helpers for the Keel platform and services built on top of it.
Design philosophy ¶
testkit is a helper library, not a test framework. It reduces setup boilerplate without hiding behavior, wrapping stdlib assertions, or creating a custom assertion language. Every helper accepts *testing.T to enforce test-only usage and calls t.Fatal / t.Error directly so failures are attributed to the correct line in the test.
Architecture overview ¶
The package is organized into four concerns:
Clock helpers (clock.go): FakeClock for deterministic, injectable time sources. Production code accepts a Clock interface; tests inject FakeClock.
DB fixture helpers (dbfixture.go): Transaction-scoped fixtures that roll back automatically on test cleanup, plus utilities for truncation and row counting. Requires a *sql.DB; no real database is needed for unit tests.
HTTP helpers (httptest.go): Request builders and response assertion helpers that wrap net/http/httptest without creating a custom client or assertion DSL.
Fake auth helpers (fakeauth.go): FakeVerifier and FakeStateChecker that implement the platform/auth interfaces, plus NewPrincipal / WithFakePrincipal for injecting identities directly into request contexts.
Integration bootstrap helpers (bootstrap.go): Package-level TestMain wrappers, environment variable helpers, and setup/teardown patterns for integration test suites.
Quick start: Clock ¶
fc := testkit.NewFakeClock(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)) svc := myservice.New(myservice.WithClock(fc)) fc.Advance(5 * time.Minute) // svc now sees 2026-01-01 00:05:00 UTC.
Quick start: DB fixtures ¶
func TestRepository(t *testing.T) {
db := openTestDB(t) // helper that opens *sql.DB
tx := testkit.TxFixture(t, db) // rolls back on t.Cleanup
testkit.MustExec(t, tx, "INSERT INTO items VALUES ($1, $2)", "id-1", "name-1")
// run repo method, assert results
}
Quick start: HTTP helpers ¶
req := testkit.NewRequest(t, http.MethodPost, "/items",
testkit.WithJSON(t, body),
testkit.WithBearer("fake-token"),
)
rr := testkit.RunHandler(t, myHandler, req)
testkit.AssertStatus(t, rr, http.StatusCreated)
var got ItemResponse
testkit.AssertJSON(t, rr, &got)
Quick start: Fake auth ¶
// Bypass auth middleware entirely by injecting a principal into context.
p := testkit.NewPrincipal("user-123", testkit.WithRole("admin"))
ctx := testkit.WithFakePrincipal(r.Context(), p)
// Or use FakeVerifier in middleware tests.
verifier := testkit.NewFakeVerifier(p, nil)
mw := auth.Middleware(verifier)
Quick start: Integration bootstrap ¶
func TestMain(m *testing.M) {
testkit.MainWithEnv(m,
testkit.RequireEnv("DATABASE_URL"),
)
}
func TestIntegration(t *testing.T) {
testkit.SkipIfShort(t)
// long-running test body
}
Integration test patterns ¶
Integration tests connect to external dependencies (databases, message queues, HTTP servers). The following conventions apply throughout the platform:
Guard integration tests with testkit.SkipIfShort(t) so they are excluded from unit test runs triggered by "go test -short ./...".
Use testkit.MainWithEnv in TestMain to assert that required environment variables are present before running any tests in the package. Missing variables cause a clear, actionable failure rather than a cryptic nil pointer or connection error inside a test.
Isolate database state: use TxFixture for row-level isolation (fastest) or CleanupDB for truncation-based isolation between tests.
Store shared, expensive resources (DB pools, containers) in package-level variables initialized in TestMain, not in individual tests.
Security model ¶
FakeVerifier, FakeStateChecker, NewPrincipal, and WithFakePrincipal are TEST-ONLY constructs. They bypass all cryptographic verification and must never be linked into production binaries. The helpers accept *testing.T (or are documented as TEST-ONLY) to make misuse visible at code review.
Importing testkit in non-test files is a security violation that the Go build system will not catch automatically; code review must enforce this.
fakeauth.go provides fake authentication helpers for handler and middleware tests.
These helpers are TEST-ONLY. They must never be used in production deployments. Using FakeVerifier or WithFakePrincipal outside test code is a security violation.
Usage:
// Build a principal for the test.
p := testkit.NewPrincipal("user-123", testkit.WithRole("admin"))
// Inject principal directly into a request context (bypass middleware).
ctx := testkit.WithFakePrincipal(r.Context(), p)
// Or use FakeVerifier as a TokenVerifier in middleware tests.
verifier := testkit.NewFakeVerifier(p, nil)
mw := auth.Middleware(verifier)
hmac_verifier.go — HMAC-SHA256 test token verifier and token factory.
Issue #159: moved from platform/auth to platform/testkit to enforce the test-only boundary at the package level. Production code must not import platform/testkit; build systems can enforce this via import-graph rules.
HMACVerifier and NewTestToken are TEST-ONLY. They must not be used in production deployments. The shared-secret design is not appropriate for multi-party or public-key-based identity systems.
For development-mode builds (//go:build dev), the devmode.go files in each cmd import this package explicitly — that is intentional and safe because //go:build dev is never compiled into production binaries.
httptest.go provides HTTP test helpers for handler and integration tests.
These helpers reduce setup boilerplate for request building and response inspection without creating a custom assertion framework. Standard library testing patterns remain valid alongside these helpers.
All helpers are TEST-ONLY and accept *testing.T to enforce this.
Usage:
// Build a request and run it against an http.Handler. req := testkit.NewRequest(t, http.MethodPost, "/items", testkit.WithJSON(body)) rr := testkit.RunHandler(t, myHandler, req) testkit.AssertStatus(t, rr, http.StatusCreated) testkit.AssertHeader(t, rr, "Content-Type", "application/json") var got ItemResponse testkit.AssertJSON(t, rr, &got)
Index ¶
- func AssertHeader(t *testing.T, rr *httptest.ResponseRecorder, key, want string)
- func AssertJSON(t *testing.T, rr *httptest.ResponseRecorder, dst any)
- func AssertProblemJSON(t *testing.T, rr *httptest.ResponseRecorder) map[string]any
- func AssertStatus(t *testing.T, rr *httptest.ResponseRecorder, want int)
- func CleanupDB(t *testing.T, db *sql.DB, tables ...string)
- func CountRows(t *testing.T, db sqlQueryer, table string, whereClause string, args ...any) int
- func ExecFixture(t *testing.T, db *sql.DB, query string, args ...any) *sql.Tx
- func GetEnvOrFail(t *testing.T, key string) string
- func GetEnvOrSkip(t *testing.T, key string) string
- func MainWithEnv(m *testing.M, reqs ...EnvRequirement)
- func MustExec(t *testing.T, db sqlExecer, query string, args ...any)
- func MustQuery(t *testing.T, db sqlQueryer, query string, dest ...any)
- func NewPrincipal(subject string, opts ...PrincipalOption) auth.Principal
- func NewRequest(t *testing.T, method, target string, opts ...RequestOption) *http.Request
- func NewTestToken(secret []byte, claims map[string]any) (string, error)
- func RunHandler(t *testing.T, handler http.Handler, req *http.Request) *httptest.ResponseRecorder
- func SetupTest(t *testing.T, setups ...SetupFunc)
- func SkipIfShort(t *testing.T)
- func TruncateTables(t *testing.T, db *sql.DB, tables ...string)
- func TxFixture(t *testing.T, db *sql.DB) *sql.Tx
- func WithFakePrincipal(ctx context.Context, p auth.Principal) context.Context
- type Clock
- type EnvRequirement
- type FakeClock
- func (c *FakeClock) Advance(d time.Duration)
- func (c *FakeClock) Now() time.Time
- func (c *FakeClock) Reset(t time.Time)
- func (c *FakeClock) Set(t time.Time)
- func (c *FakeClock) Since(t time.Time) time.Duration
- func (c *FakeClock) Snapshot(step time.Duration) time.Time
- func (c *FakeClock) Until(t time.Time) time.Duration
- type FakeStateChecker
- type FakeVerifier
- type HMACVerifier
- type PrincipalOption
- type RealClock
- type RequestOption
- type SetupFunc
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AssertHeader ¶
func AssertHeader(t *testing.T, rr *httptest.ResponseRecorder, key, want string)
AssertHeader fails the test if the response header key does not contain want. The comparison is exact (case-preserving as stored by ResponseRecorder).
TEST-ONLY: must not be called from production code.
func AssertJSON ¶
func AssertJSON(t *testing.T, rr *httptest.ResponseRecorder, dst any)
AssertJSON decodes the response body into dst and fails the test on error. Callers can then assert fields on dst using standard Go comparisons.
TEST-ONLY: must not be called from production code.
func AssertProblemJSON ¶
AssertProblemJSON decodes the response body as an RFC 7807 problem document and returns it. Fails the test if decoding fails. The caller can assert individual fields on the returned map.
TEST-ONLY: must not be called from production code.
func AssertStatus ¶
func AssertStatus(t *testing.T, rr *httptest.ResponseRecorder, want int)
AssertStatus fails the test if the recorded response code does not match want.
TEST-ONLY: must not be called from production code.
func CleanupDB ¶
CleanupDB registers a cleanup function that truncates all provided tables when the test ends. Useful to reset shared test databases between tests.
TEST-ONLY: must not be called from production code.
func CountRows ¶
CountRows returns the number of rows in table that match the optional WHERE clause. whereClause may be empty for a full table count. Fails the test on any SQL error.
TEST-ONLY: must not be called from production code.
func ExecFixture ¶
ExecFixture executes one or more SQL statements on db within a new transaction. The transaction is rolled back at test cleanup regardless of success or failure. Returns the transaction for use in subsequent fixture writes.
Each statement is executed with the provided args in order. For multiple statements with different args, call ExecFixture once per statement.
TEST-ONLY: must not be called from production code.
func GetEnvOrFail ¶
GetEnvOrFail returns the value of the environment variable named key. If the variable is missing or empty the test is failed immediately with a clear diagnostic message. Use this when the variable is unconditionally required for the test to be meaningful.
TEST-ONLY: must not be called from production code.
func GetEnvOrSkip ¶
GetEnvOrSkip returns the value of the environment variable named key. If the variable is missing or empty the test is skipped with a clear message. Use this for test-specific optional dependencies that should skip cleanly instead of failing when the resource is unavailable.
TEST-ONLY: must not be called from production code.
func MainWithEnv ¶
func MainWithEnv(m *testing.M, reqs ...EnvRequirement)
MainWithEnv is a drop-in TestMain body for integration test packages. It checks that every EnvRequirement is satisfied, then delegates to m.Run(). If any required variable is missing the suite exits with code 1 and a clear diagnostic message listing all missing variables.
Usage:
func TestMain(m *testing.M) {
testkit.MainWithEnv(m, testkit.RequireEnv("DATABASE_URL"))
}
func MustExec ¶
MustExec executes query on the given queryer (tx or db), failing the test on error. Use this for fixture setup inside a pre-existing transaction.
TEST-ONLY: must not be called from production code.
func MustQuery ¶
MustQuery executes a query and scans the first row into dest. Fails the test if no rows are returned or scanning fails.
TEST-ONLY: must not be called from production code.
func NewPrincipal ¶
func NewPrincipal(subject string, opts ...PrincipalOption) auth.Principal
NewPrincipal builds a fake auth.Principal for use in tests. subject is used as the Principal.Subject (JWT sub claim). Default values: Issuer = "test-issuer", ExpiresAt = 15 minutes from now.
Must only be used in test code.
func NewRequest ¶
NewRequest builds an *http.Request for use in handler tests. method and target follow the httptest.NewRequest conventions. Options are applied in order after the request is constructed.
TEST-ONLY: must not be called from production code.
func NewTestToken ¶
NewTestToken creates a signed JWT-shaped token for use in tests.
The token header is always {"alg":"HS256","typ":"JWT"}. The provided claims map becomes the payload; callers are responsible for supplying all required claims (iss, sub, aud, exp, iat, jti).
This function must not be used in production code.
func RunHandler ¶
RunHandler calls handler.ServeHTTP with req and returns the recorded response. The handler is invoked synchronously; the returned ResponseRecorder is fully populated when RunHandler returns.
TEST-ONLY: must not be called from production code.
func SetupTest ¶
SetupTest runs each setup function in order, registering all returned cleanup functions via t.Cleanup. Cleanup functions run in LIFO order when the test finishes, matching the defer convention.
Usage:
func TestSomething(t *testing.T) {
testkit.SetupTest(t,
setupDatabase,
setupMessageQueue,
)
// test body; teardown runs automatically via t.Cleanup
}
TEST-ONLY: must not be called from production code.
func SkipIfShort ¶
SkipIfShort skips t immediately when the -short flag is active. Use this to gate integration tests that connect to external dependencies.
Convention: all integration tests that touch a real database, message queue, or network service must call SkipIfShort at the top of their test body.
TEST-ONLY: must not be called from production code.
func TruncateTables ¶
TruncateTables truncates the given tables on db. Table names are validated to contain only identifier-safe characters. Fails the test on any error.
TEST-ONLY: must not be called from production code.
func TxFixture ¶
TxFixture opens a transaction on db and returns it. The transaction is automatically rolled back when the test ends, ensuring no fixture data escapes into the DB.
TEST-ONLY: must not be called from production code.
func WithFakePrincipal ¶
WithFakePrincipal injects p directly into ctx using the same context key as auth.WithPrincipal. Use this to simulate an already-authenticated request without going through middleware or token verification.
Must only be used in test code.
Types ¶
type Clock ¶
Clock is the minimal interface for injectable time sources. Platform code and project services that need time should accept a Clock rather than calling time.Now directly, so tests can inject FakeClock.
type EnvRequirement ¶
type EnvRequirement struct {
// contains filtered or unexported fields
}
EnvRequirement describes a required environment variable for an integration test suite. Build requirements with RequireEnv.
func RequireEnv ¶
func RequireEnv(key string, description ...string) EnvRequirement
RequireEnv declares that the environment variable named key must be set and non-empty before the integration test suite runs. An optional human-readable description is appended to the missing-variable error message.
func (EnvRequirement) Description ¶
func (r EnvRequirement) Description() string
Description returns the optional human-readable description for this requirement.
func (EnvRequirement) Key ¶
func (r EnvRequirement) Key() string
Key returns the environment variable name for this requirement.
type FakeClock ¶
type FakeClock struct {
// contains filtered or unexported fields
}
FakeClock is a deterministic, thread-safe clock for use in tests. The zero value is not usable; use NewFakeClock to create one.
FakeClock does not drive timers or tickers automatically. Tests must call Advance or Set explicitly to move time forward.
func NewFakeClock ¶
NewFakeClock returns a FakeClock starting at t. t must not be the zero value.
func (*FakeClock) Advance ¶
Advance moves the clock forward by d. d must be positive; passing zero or a negative duration panics to catch test logic errors early.
func (*FakeClock) Reset ¶
Reset sets the clock to t without monotonic enforcement. Use this when a test needs to start from a fresh baseline mid-test.
func (*FakeClock) Set ¶
Set moves the clock to exactly t. t must be after the current time to prevent accidental time reversal in tests that depend on monotonic ordering. Use Reset to set an earlier time intentionally.
func (*FakeClock) Since ¶
Since returns the duration elapsed since t according to the fake clock. Equivalent to c.Now().Sub(t).
type FakeStateChecker ¶
type FakeStateChecker struct {
// contains filtered or unexported fields
}
FakeStateChecker is a test-only SubjectStateChecker that returns a fixed error. Use nil error to simulate a passing state check; a non-nil error to simulate revocation.
func NewFakeStateChecker ¶
func NewFakeStateChecker(err error) *FakeStateChecker
NewFakeStateChecker creates a FakeStateChecker returning err on every Check call.
func (*FakeStateChecker) Calls ¶
func (f *FakeStateChecker) Calls() int
Calls returns the number of times Check has been called.
type FakeVerifier ¶
type FakeVerifier struct {
// contains filtered or unexported fields
}
FakeVerifier is a test-only TokenVerifier that returns a fixed principal and error for any token value. It is designed for use in middleware tests and handler tests that need to control the authentication outcome.
Must only be used in test code.
func NewFakeVerifier ¶
func NewFakeVerifier(principal auth.Principal, err error) *FakeVerifier
NewFakeVerifier creates a FakeVerifier that returns principal and err on every Verify call.
Pass err = nil to simulate successful authentication. Pass a non-nil error to simulate authentication failure.
func (*FakeVerifier) Calls ¶
func (f *FakeVerifier) Calls() int
Calls returns the number of times Verify has been called. Useful for asserting that middleware called the verifier the expected number of times.
type HMACVerifier ¶
type HMACVerifier struct {
// Secret is the shared HMAC-SHA256 signing key.
// Must not be empty; Verify returns an error if it is.
Secret []byte
}
HMACVerifier is a test-only auth.TokenVerifier that validates JWT-shaped tokens signed with HMAC-SHA256.
Token structure: base64url(header).base64url(payload).base64url(signature) where the signature covers "header.payload" using the configured Secret.
Required payload claims: iss, sub, aud, exp, iat, jti. Optional claims mapped to Principal: sid, roles, scope, ver.
type PrincipalOption ¶
PrincipalOption configures a Principal built by NewPrincipal.
func WithAudience ¶
func WithAudience(aud ...string) PrincipalOption
WithAudience sets the audience list on the fake principal.
func WithIssuer ¶
func WithIssuer(iss string) PrincipalOption
WithIssuer sets the issuer on the fake principal.
func WithRole ¶
func WithRole(role string) PrincipalOption
WithRole adds a role to the fake principal.
func WithScope ¶
func WithScope(scope string) PrincipalOption
WithScope adds a scope to the fake principal.
func WithTokenVersion ¶
func WithTokenVersion(v int) PrincipalOption
WithTokenVersion sets the token version on the fake principal.
type RealClock ¶
type RealClock struct{}
RealClock is a Clock backed by the system clock. Use this in production wiring; inject FakeClock in tests.
type RequestOption ¶
RequestOption configures a test HTTP request built by NewRequest.
func WithBearer ¶
func WithBearer(token string) RequestOption
WithBearer sets the Authorization header to "Bearer <token>".
func WithHeader ¶
func WithHeader(key, value string) RequestOption
WithHeader sets a single request header to value.