entrest

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

entc.go
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:

handler_test.go
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:

handler_test.go
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:

handler_test.go
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:

ArgumentPurpose
ctxRequest context — forwarded to httptest.NewRequest
methodHTTP method (http.MethodGet, http.MethodPost, …)
pathPath relative to the handler (no host)
bodyJSON-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]:

handler_test.go
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:

FieldContents
Data*httptest.ResponseRecorder — status, headers, raw body
ValueUnmarshalled 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:

handler_test.go
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:

handler_test.go
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:

handler_test.go
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:

handler_test.go
pets := db.Pet.CreateBulk(enttest.Multiple(newPet, db, 25)...).SaveX(ctx)
_ = pets // use in assertions

where newPet returns *ent.PetCreate:

handler_test.go
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:

handler_test.go
srv := enttest.NewServer(t, db, nil).WithLogResponses(true)

Last updated on

On this page