Skip to content

Testing

Tests run against a real Postgres: the queries, the constraints and the types are what they check. testDatabase(), from @tetsujs/sql/testing, gives a test file a database of its own:

src/orders.test.ts
import { expect, test } from "bun:test";
import { testDatabase } from "@tetsujs/sql/testing";
import { schema } from "./db.generated";
import { customerOrders } from "./queries.generated";
const {
const db: Kysely<DB>

Kysely on it, as database() makes it with the options given beside url and dir. It connects on its first query, which a test makes once the database exists.

db
} = testDatabase(schema, {
url: string | undefined

A postgres:// URL of a database on the server the tests may create databases on: its user needs that right. It is usually read from the environment, so undefined is taken, and refused saying so.

url
: Bun.
const env: Bun.Env & NodeJS.ProcessEnv & ImportMetaEnv

The environment variables of the process

Defaults to process.env as it was when the current Bun process launched.

Changes to process.env at runtime won't automatically be reflected in the default value. For that, you can pass process.env explicitly.

env
.DATABASE_URL,
dir: string

The directory of the migration files, from the working directory.

dir
: "migrations",
});
test("lists a customer's orders, with the shipment of each", async () => {
const
const customer: {
id: bigint;
}
customer
= await
const db: Kysely<DB>

Kysely on it, as database() makes it with the options given beside url and dir. It connects on its first query, which a test makes once the database exists.

db
.insertInto("customers")
.values({
email: ValueExpression<DB, "customers", string>
email
: "ada@example.com",
name: string
name
: "Ada" })
.returning("id")
.executeTakeFirstOrThrow();
const
const order: {
id: bigint;
}
order
= await
const db: Kysely<DB>

Kysely on it, as database() makes it with the options given beside url and dir. It connects on its first query, which a test makes once the database exists.

db
.insertInto("orders")
.values({
customer_id: ValueExpression<DB, "orders", bigint>
customer_id
:
const customer: {
id: bigint;
}
customer
.
id: bigint
id
,
total: ValueExpression<DB, "orders", string>
total
: "12.50",
status?: ValueExpression<DB, "orders", "new" | "paid" | "shipped" | undefined>
status
: "paid" })
.returning("id")
.executeTakeFirstOrThrow();
await
const db: Kysely<DB>

Kysely on it, as database() makes it with the options given beside url and dir. It connects on its first query, which a test makes once the database exists.

db
.insertInto("shipments")
.values({
order_id: ValueExpression<DB, "shipments", bigint>
order_id
:
const order: {
id: bigint;
}
order
.
id: bigint
id
,
carrier: ValueExpression<DB, "shipments", string>
carrier
: "post" })
.execute();
expect(await customerOrders(
const db: Kysely<DB>

Kysely on it, as database() makes it with the options given beside url and dir. It connects on its first query, which a test makes once the database exists.

db
, {
customerId: bigint
customerId
:
const customer: {
id: bigint;
}
customer
.
id: bigint
id
})).toEqual([
{
id: bigint
id
:
const order: {
id: bigint;
}
order
.
id: bigint
id
,
status: OrderStatus
status
: "paid",
total: string
total
: "12.50",
carrier: string | null
carrier
: "post",
shipped_at: Date | null
shipped_at
: expect.any(Date),
},
]);
});

testDatabase() registers two hooks of bun:test where it is called — at the top level of a file for the file, in a describe for that describe — and returns the database’s db, its url for code under test that opens connections of its own, and its name. Its options besides url and dir are database()’s, such as max and connection, so that the tests run on a pool set as the application’s; password, tls and path reach the server as the database is made, too.

  • beforeAll creates a database from template0 on the server url names, and applies the migrations of dir. It fails when the newest migration is not the one the types of schema were built from: the tests would check code against types older or newer than the database. Run types:generate again.
  • afterAll closes the pool, then drops the database, whoever is still on it.

The server is one where tests may create databases, such as a container. A url that is not set is refused, saying so, so Bun.env.DATABASE_URL can be passed as it is.

Every test of the file runs on the same database, one after another. A test inserts the rows it reads, as above, so that it runs the same alone, under -t, as with the others. Two files never share a database.

Bun runs hooks in the order they were registered. Call testDatabase() before the hooks of your own that fill the database, and leave cleaning up to the drop: an afterAll of your own runs once the database is gone. When no test of the scope runs, as with a -t filter that matches none, Bun runs neither hook, and no database is made.

A pool that does not close fails afterAll, once the database is dropped all the same: a test left a transaction open, or a query running.

A run that was killed, or stopped by --bail, which skips afterAll, leaves its database behind. Each is named tetsu_sql_ and a UUIDv7, which holds the time it was made, and the next run on the server drops it once it is an hour old and nobody is on it.

The same database serves an application under test, built as main.ts builds it, with serve():

src/orders.http.test.ts
import { expect, test } from "bun:test";
import { createApp } from "@tetsujs/core";
import { serve } from "@tetsujs/core/testing";
import { testDatabase } from "@tetsujs/sql/testing";
import { schema } from "./db.generated";
import { ordersController } from "./orders";
const {
const db: Kysely<DB>

Kysely on it, as database() makes it with the options given beside url and dir. It connects on its first query, which a test makes once the database exists.

db
} = testDatabase(schema, {
url: string | undefined

A postgres:// URL of a database on the server the tests may create databases on: its user needs that right. It is usually read from the environment, so undefined is taken, and refused saying so.

url
: Bun.
const env: Bun.Env & NodeJS.ProcessEnv & ImportMetaEnv

The environment variables of the process

Defaults to process.env as it was when the current Bun process launched.

Changes to process.env at runtime won't automatically be reflected in the default value. For that, you can pass process.env explicitly.

env
.DATABASE_URL,
dir: string

The directory of the migration files, from the working directory.

dir
: "migrations",
});
const
const request: RequestFn
request
= serve(createApp({ routes: ordersController(
const db: Kysely<DB>

Kysely on it, as database() makes it with the options given beside url and dir. It connects on its first query, which a test makes once the database exists.

db
) }));
test("refuses an order for a customer there is not", async () => {
const
const response: Response
response
= await
const request: RequestFn
(path: string, init?: RequestInit) => Promise<Response>
request
("/customers/999/orders", {
method?: string | undefined

A string to set request's method.

method
: "POST",
headers?: HeadersInit | undefined

A Headers object, an object literal, or an array of two-item arrays to set request's headers.

headers
: { "content-type": "application/json" },
body?: BodyInit | null | undefined

A BodyInit object or null to set request's body.

body
: JSON.stringify({
total: string
total
: "1.00" }),
});
expect(
const response: Response
response
.
status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
).toBe(404);
});

A Postgres for the tests in CI is a service of the job: Deploy and CI has one.