Skip to content

Testing

What is tested, how endpoint tests run against real infrastructure, and the helpers the framework provides.

Terminal window
bun run db:up # PostgreSQL :54329 + Redis :63799 (compose)
cp .env.test.example .env.test # TEST_DATABASE_URL=postgres://… (a server-level admin URL)

TEST_DATABASE_URL must point at a dedicated test server/admin URL — never the application’s DATABASE_URL; the testing package refuses to create test databases on the application target or in APP_ENV=production.

Every database test gets its own database (fw_test_<random>), created from TEST_DATABASE_URL, migrated from the app’s database/migrations, and dropped in success and failure paths. Nothing leaks between runs.

Application-level tests use the generated tests/support.ts:

const ctx = await createTestContext(); // boots the full app on a fresh migrated DB
const response = await ctx.client.request('/api/v1/products', jsonInit('POST', { name: 'x', price: '1' }));
await ctx.close(); // stops server, shuts down, drops the DB

TEST_MODE=network swaps in-process http.request for a real loopback listener (same assertions, real transport). The generated endpoint suites run identically in both modes — make:crud --verify --test-mode network proves routes over the wire.

Each make:crud module ships tests/<resource>.crud.api.test.ts, which calls the registered Hono application (never a mocked controller):

  • happy path: create → list → read → update → re-read → delete → 404
  • public allowlist exposure (secret fields never serialized)
  • deterministic, bounded listing; pagination bounds (422)
  • malformed JSON (400), malformed ids (422), unknown ids (404)
  • invalid payloads: missing / mistyped / refinement-violating / forbidden fields
  • empty updates, immutable fields (createdAt)

make:crud --verify runs this suite plus a typecheck after every generation and reports each route, pass/fail counts and database cleanup in the result (JSON with --json).

Package tests (packages/*/tests) exercise real adapters where it matters: CRUD mixins, auditing, transactions and pagination run on real PostgreSQL; queues run on real Redis; events tests cover both buses. Pattern:

const database = await createTestDatabase({ adminUrl: requireTestDatabaseUrl() });
const connection = createConnection({ url: database.url, maxConnections: 4 });
await connection.sql`create table …`;
// … assertions …
await connection.close();
await database.drop();

MemoryEventBus stands in for Redis in unit tests; the testing package provides isolated databases instead of a fake query layer. Fakes prove wiring, never infrastructure behavior — retry, duplicate delivery and failure semantics are only proven by the real-adapter suites.

  • packages/cli/tests/generated-app.test.ts — make:crud end to end on new and existing tables, route collisions, --dry-run writing nothing, and the generated application’s full suite.
  • packages/cli/tests/create-app.test.ts — the create release gate: the generated application typechecks, passes its suite, migrates the audit table, accepts make:crud --verify, and create --with-demo produces a working products module whose endpoint tests pass as-is.
  • apps/playground/tests/contract.test.ts — OpenAPI drift check and a typed Hono RPC client against the composed application.

Run everything from the workspace root:

Terminal window
bun test # or: bun run test
  • No skipped-if-missing tests: missing TEST_DATABASE_URL fails with setup instructions instead of silently passing.
  • Tests never auto-migrate the application database; they only migrate their own isolated database.
  • Ephemeral network servers bind 127.0.0.1:0 and always close.