HTTP & Feature Tests
> Spin up an in-memory Velocity app, drive your router with a fluent HTTP client, chain expressive response assertions, and record events and commands with fakes.
The in-memory app harness, a fluent HTTP client that drives your router and asserts against the response, handler-level context for unit tests, and fakes that record what your code dispatched.
Test App
velocitytest.NewApp builds an *velocity.App wired with the in-memory defaults a test needs: memory cache, memory queue, console logger, log-mail driver, and APP_ENV=testing (which opts out of the APP_KEY requirement). It lives in its own subpackage so production builds never pull in those defaults.
import "github.com/velocitykode/velocity/velocitytest"
func TestSignup(t *testing.T) {
app, err := velocitytest.NewApp()
if err != nil {
t.Fatal(err)
}
defer app.Shutdown(context.Background())
// register routes, then drive them with the HTTP client below
}It accepts the same Option funcs as velocity.New(), so you can layer config or swap services. WithoutEvents() skips event dispatching; WithFakeEvents(fake) swaps in a recording dispatcher (see Fakes).
HTTP Tests
testing/http.NewTestClient wraps a router (any http.Handler) and gives you request verbs that return a *TestResponse. Assertions are chainable and fail the test through the *testing.T you pass in.
import velhttp "github.com/velocitykode/velocity/testing/http"
func TestEcho(t *testing.T) {
client := velhttp.NewTestClient(t, router)
client.PostJSON("/echo", map[string]any{"name": "test"}).
AssertOk().
AssertJSON("received.name", "test").
AssertJSONPath("data.status", "active")
}Request verbs
client.Get("/path")
client.Post("/path", body) // body is an io.Reader
client.PostJSON("/path", data) // marshals data, sets Content-Type
client.Put("/path", body)
client.PutJSON("/path", data)
client.Patch("/path", body)
client.PatchJSON("/path", data)
client.Delete("/path")Builder methods stack request state before a verb:
client.WithHeader("X-Request-Id", "abc").
WithToken("jwt-token"). // sets Authorization: Bearer
WithCookie(&http.Cookie{...}).
Get("/me")Response assertions
Status:
resp.AssertStatus(418)
resp.AssertOk() // 200
resp.AssertCreated() // 201
resp.AssertNoContent() // 204
resp.AssertNotFound() // 404
resp.AssertForbidden() // 403
resp.AssertUnauthorized() // 401
resp.AssertUnprocessable() // 422
resp.AssertRedirect("/login") // location optionalHeaders and cookies:
resp.AssertHeader("Content-Type", "application/json")
resp.AssertHeaderMissing("X-Debug")
resp.AssertCookie("session", "value")
resp.AssertCookieMissing("session")Body and JSON:
resp.AssertBodyContains("Hello")
resp.AssertBodyEmpty()
resp.AssertJSON("user.name", "Alice") // dotted key
resp.AssertJSONPath("data.address.city", "Portland")
resp.AssertJSONCount(3, "posts")
resp.AssertJSONStructure([]string{"id", "name", "email"})Authentication
Act as a user for the request, or assert the resulting auth state:
client.ActingAs(guard, user).Get("/dashboard").AssertOk()
client.ActingAsID(guard, userID).Get("/dashboard").AssertOk()
client.AssertAuthenticated(guard)
client.AssertGuest(guard)Validation
When a route runs validation, assert the outcome directly:
resp := client.PostJSON("/signup", map[string]any{"email": "bad"})
resp.AssertInvalid("email") // these fields failed
resp.AssertValidationErrors(map[string][]string{
"email": {"The email must be a valid email address."},
})
resp.AssertValid() // no validation errorsUnit-Level Context
For handler unit tests without a full app or router, router.NewTestContext returns a *Context backed by an *httptest.ResponseRecorder:
ctx, rec := router.NewTestContext("POST", "/users", body)
err := MyHandler(ctx)
if rec.Code != 201 {
t.Fatalf("got %d", rec.Code)
}Fakes
Events
events.NewFakeDispatcher() records dispatched events instead of running listeners, so you can assert what fired. Wire it via WithFakeEvents or set it on the app directly.
fake := events.NewFakeDispatcher()
fake.AssertDispatched(UserRegistered{}, func(e interface{}) bool {
return e.(UserRegistered).Email == "a@b.com"
})
fake.AssertDispatchedTimes(UserRegistered{}, 1)
fake.AssertNotDispatched(PaymentFailed{})
fake.AssertNothingDispatched()Command Bus
bus.NewFakeBus() records commands (sync and async) for the same style of assertion.
fake := bus.NewFakeBus()
fake.AssertDispatched(CreateUser{}, func(c bus.Command) bool { ... })
fake.AssertDispatchedTimes(CreateUser{}, 1)
fake.AssertNotDispatched(DeleteUser{})
fake.AssertNothingDispatched()
// async path
fake.AssertAsyncDispatched(SendEmail{}, func(c bus.Command) bool { ... })
fake.AssertAsyncDispatchedTimes(SendEmail{}, 1)
fake.AssertAsyncNotDispatched(SendEmail{})
fake.AssertNothingAsyncDispatched()For seeding a real database and writing full integration tests, see Database & Factories.