Testing
Generated helpers for integration-testing the HTTP handler against a real Ent client.
How-to guide
Steps for enabling and using the generated enttest request helpers. This page does not cover
authentication, privacy policies, or testing the /openapi.json and /docs routes — see
Authentication & Permissions and API Docs.
Set entrest.Config.WithTesting to true at
generation time and entrest adds request helpers to your existing <ent>/enttest package. They wrap
httptest around the generated handler so you can exercise real routes, query params, and JSON bodies
against an in-memory database.
WithTesting is ignored when Handler is
HandlerNone — there is no handler to test.
Enable generation
ex, err := entrest.NewExtension(&entrest.Config{
Handler: entrest.HandlerStdlib,
WithTesting: true,
})
if err != nil {
log.Fatal(err)
}
err = entc.Generate("./schema", &gen.Config{
Target: "./ent",
Package: "example.com/app/ent",
}, entc.Extensions(ex))Run go generate as usual. The helpers land in ent/enttest alongside Ent's own enttest.Open helpers.
Minimal example
After enabling WithTesting, a complete test looks like this:
func TestGetPet(t *testing.T) {
ctx := context.Background()
db := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_pragma=foreign_keys(1)")
t.Cleanup(func() { db.Close() })
srv := enttest.NewServer(t, db, nil)
pet := db.Pet.Create().SetName("Riley").SetAge(3).SaveX(ctx)
resp := srv.Request[ent.Pet](ctx, http.MethodGet, "/pets/"+strconv.Itoa(pet.ID), nil).Must(t)
assert.Equal(t, pet.ID, resp.Value.ID)
}Paths are relative to the handler root — with the stdlib handler, BasePath stripping is applied for
you, so use /pets rather than /v1/pets when BasePath is /v1.
Test setup
For shared setup across tests, extract a helper:
func newTestServer(t *testing.T, cfg *rest.ServerConfig) (context.Context, *ent.Client, *enttest.TestServer) {
t.Helper()
ctx := context.Background()
db := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_pragma=foreign_keys(1)")
t.Cleanup(func() { db.Close() })
srv := enttest.NewServer(t, db, cfg)
return ctx, db, srv
}NewServer calls rest.NewServer and serves the returned handler. Pass nil for ServerConfig when
defaults are fine — see Runtime Configuration.
Seed data through the Ent client (db.Pet.Create()...SaveX(ctx)), then hit the API with srv.Request.
Making requests
Request is generic over the expected JSON response type:
ctx, db, srv := newTestServer(t, nil)
pet := db.Pet.Create().SetName("Riley").SetAge(3).SaveX(ctx)
// GET a single entity.
resp := srv.Request[ent.Pet](ctx, http.MethodGet, "/pets/"+strconv.Itoa(pet.ID), nil).Must(t)
assert.Equal(t, http.StatusOK, resp.Data.Code)
assert.Equal(t, pet.ID, resp.Value.ID)
// POST with a JSON body (Content-Type is set automatically).
created := srv.Request[ent.Pet](ctx, http.MethodPost, "/pets", map[string]any{
"name": "Orea",
"age": 2,
}).Must(t)
assert.Equal(t, http.StatusCreated, created.Data.Code)Arguments:
| Argument | Purpose |
|---|---|
ctx | Request context — forwarded to httptest.NewRequest |
| method | HTTP method (http.MethodGet, http.MethodPost, …) |
| path | Path relative to the handler (no host) |
| body | JSON-marshalable value, nil, or http.NoBody |
Use http.NoBody on GET/DELETE when you want an explicit empty body instead of nil.
List responses
List endpoints decode into rest.PagedResponse[T]:
resp := srv.Request[rest.PagedResponse[ent.Pet]](
ctx, http.MethodGet, "/pets?page=2&per_page=10", nil,
).Must(t)
assert.Equal(t, 2, resp.Value.Page)
assert.Len(t, resp.Value.Content, 10)See Pagination for the response fields and query parameters.
Empty bodies
Pass rest.PagedResponse[ent.Pet] (or any type) for successful 204 No Content responses — the body
is not decoded. For deletes where you only care about the status, Request[string] also works.
Success and failure
Response[T] holds three fields:
| Field | Contents |
|---|---|
Data | *httptest.ResponseRecorder — status, headers, raw body |
Value | Unmarshalled JSON on 2xx (except 204) |
Error | *rest.ErrorResponse on non-2xx |
Call .Must(t) when you expect success — it fails the test if Error is set:
resp := srv.Request[ent.Pet](ctx, http.MethodGet, "/pets/99999", nil)
assert.Equal(t, http.StatusNotFound, resp.Data.Code)
require.NotNil(t, resp.Error)
assert.Equal(t, http.StatusNotFound, resp.Error.Code)Skip .Must(t) whenever you are asserting on a specific error status. The error shape matches
Configuration — Error handling.
Custom handlers
If your production server wraps the generated handler in middleware, use enttest.WithExisting instead
of NewServer:
srv, err := rest.NewServer(db, &rest.ServerConfig{MaxRequestBodyBytes: 100})
require.NoError(t, err)
ts := enttest.WithExisting(t, srv.Handler())
resp := ts.Request[ent.User](ctx, http.MethodPost, "/users", strings.Repeat("x", 101))
require.NotNil(t, resp.Error)
assert.Equal(t, http.StatusRequestEntityTooLarge, resp.Data.Code)Chi mounts
Build the same router you use in production and pass it to WithExisting. Set BasePath on
ServerConfig to match your mount point, and include the mount prefix in test paths:
cfg := &rest.ServerConfig{BasePath: "/v1"}
srv, err := rest.NewServer(db, cfg)
require.NoError(t, err)
r := chi.NewRouter()
r.Route("/v1", func(r chi.Router) {
srv.Handler(r)
})
ts := enttest.WithExisting(t, r)
resp := ts.Request[rest.PagedResponse[ent.Pet]](ctx, http.MethodGet, "/v1/pets", nil).Must(t)
assert.Equal(t, http.StatusOK, resp.Data.Code)See Configuration — Stdlib vs chi for mount setup.
Seeding data
enttest.Multiple runs a creator function n times and returns the results. Useful with CreateBulk:
pets := db.Pet.CreateBulk(enttest.Multiple(newPet, db, 25)...).SaveX(ctx)
_ = pets // use in assertionswhere newPet returns *ent.PetCreate:
func newPet(db *ent.Client) *ent.PetCreate {
return db.Pet.Create().SetName("test").SetAge(1)
}Debugging
Chain WithLogResponses(true) to print method, path, status, and body for every request:
srv := enttest.NewServer(t, db, nil).WithLogResponses(true)Last updated on

