Testing
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.
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.
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.
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.
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 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.
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.
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.
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)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.
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)}Chain WithLogResponses(true) to print method, path, status, and body for every request:
srv := enttest.NewServer(t, db, nil).WithLogResponses(true)
