Testing
The testing packages provide facilities for exercising HTTP handlers, whole
applications, and JSON responses.
handlertest
handlertest.New(ctx, t, handler) wraps any http.Handler in a fluent request
builder. Requests run against httptest with a fresh DI context, so injected
dependencies can be set up before each call and shared state is isolated per
test:
handlertest.New(ctx, t, handler).
Get("/users").
AssertStatusOK()The builder supports Get, Post, Put, Patch, and Delete, each with a
JSON variant that sets Accept/Content-Type headers and encodes the body
from a Go value. WithHeader(key, value) adds arbitrary headers.
HttpResult is the fluent assertion result. Status helpers cover exact matches
and ranges; body helpers compare decoded JSON, including dotted-path lookups:
| result method | effect |
|---|---|
AssertStatus(status) | exact status code |
AssertStatusOK() | any 2xx-3xx status |
AssertStatus2XX()/3XX/4XX/5XX | status within the range |
AssertStatusRange(min, max) | status within bounds |
AssertJSON(expected) | whole body equals decoded value; expected is generic, so T is inferred from the argument |
AssertJSONString(s) | whole body equals a JSON string |
AssertJSONContains(path, matcher) | value at a.b.c satisfies a match matcher |
Body() | raw response bytes |
handlertest.New(ctx, t, h).
PostJSON("/users", createUserRequest{Name: "Ada"}).
AssertStatus(http.StatusCreated).
AssertJSONContains("user.email", match.Equal("[email protected]"))kerneltest
kerneltest.TestKernel[T] boots a real kernel.Kernel with a salusaconfig
config and routes requests through k.RootHandler(). Build it once with
NewTestKernelFactory(kernel, config) and reuse per test; each call gets a
fresh DI context and a bootstrapped kernel:
var newKernel = kerneltest.NewTestKernelFactory(buildKernel(), &config.Config{})
func TestUserGet(t *testing.T) {
newKernel(t).
GetJSON("/api/users/1").
AssertStatus(http.StatusOK).
AssertJSONContains("name", match.Equal("Ada"))
}The same fluent verbs as the builder are forwarded: Get, GetJSON, Post,
PostJSON, Put, PutJSON, Patch, PatchJSON, Delete, DeleteJSON.
Because each call bootstraps the kernel with Bootstrap(ctx), providers and
services register fresh for every test.
match
The match package defines the Matcher interface used for deferred
assertions. A matcher reports whether a value matches and, on failure, a
description of the mismatch, as a Result:
type Matcher interface {
Matches(actual any) Result
}
type Result struct {
Matches bool
Description string
}Matchers compose freely:
| matcher | effect |
|---|---|
Equal(expected) | value is deeply equal to expected |
Same(expected) | value is the same pointer as expected |
Len(length) | value has the given length |
Greater(value) | value is greater than value |
GreaterOrEqual(value) | value is greater than or equal to value |
Less(value) | value is less than value |
LessOrEqual(value) | value is less than or equal to value |
All(matchers...) | satisfies every matcher |
The comparison matchers are generic over any cmp.Ordered type —
the numeric types and string — so the expected value’s type is inferred from
the argument and the actual value is type-asserted to the same type. That makes
them a natural fit for fields that are easier to bound than to pin exactly:
handlertest.New(ctx, t, h).
Get("/users").
AssertStatusOK().
AssertJSONContains("count", match.Greater(float64(0))).
AssertJSONContains("user.created_at", match.Less(float64(time.Now().Unix())))cmp.Ordered covers only the basic ordered types, so a time.Time value has
to be reduced to one of them — .Unix(), .UnixMilli(), or a string from
Format — before it can be compared. A value that fails the type assertion
simply does not match, so the expected value’s type has to match the decoded
value’s exactly. JSON bodies are decoded without json.UseNumber, which means
every JSON number surfaces as float64: comparing a JSON field against an
int or int64 literal fails the assertion even when the numbers compare
correctly.
To write a matcher that isn’t in the table, implement the Matcher interface or
convert a plain function with MatcherFunc, and return a
Result describing the mismatch when it does not hold:
func HasPrefix(prefix string) match.Matcher {
return match.MatcherFunc(func(actual any) match.Result {
s, ok := actual.(string)
if !ok || !strings.HasPrefix(s, prefix) {
return match.Result{
Matches: false,
Description: fmt.Sprintf("%v is not prefixed with %q", actual, prefix),
}
}
return match.Result{Matches: true}
})
}Assertion helpers such as AssertJSONContains
take a Matcher instead of a raw value:
handlertest.New(ctx, t, h).
Get("/users").
AssertStatusOK().
AssertJSONContains("users", match.Len(2)).
AssertJSONContains("user.name", match.All(match.Len(3), match.Equal("Ada")))