Testing
The fixture adapter for tests with zero credentials, and when to test against a real database instead.
dbsdk/testing provides a scripted adapter for unit tests. It is not an
in-memory SQL engine and cannot prove transaction or SQL correctness. What
it does prove: what your code sends, and what your code does with the
envelope and the errors.
Fixture adapter
Build the adapter and the client in two steps, so tests can also inspect what was recorded:
import { createDatabase } from "dbsdk";
import { createFixtureAdapter } from "dbsdk/testing";
const adapter = createFixtureAdapter({
fixtures: [
{
match: "select id, email from users where id = $1",
params: [42],
rows: [{ id: 42, email: "ada@example.com" }],
rowCount: 1,
command: "SELECT",
},
{
match: /insert into users/,
rows: [{ id: 43 }],
rowCount: 1,
command: "INSERT",
},
{
match: /update banned set/,
error: { code: "42501", message: "permission denied" }, // thrown as-is, then normalized
},
],
});
const db = createDatabase({ adapter });
const { rows } = await db.sql`select id, email from users where id = ${42}`;
rows[0]?.email; // "ada@example.com"
createFixtureDatabase(options) is a convenience that does both steps and
returns the client directly, for when you only need the query surface.
How matching works:
- A string
matchis compared against the statement text with whitespace normalized. A RegExpmatchis tested against the normalized text. - When
paramsis present, the query’s parameters must be deeply equal. - Fixtures are single-use by default; pass
repeat: trueto allow re-matching. - A query that matches nothing throws
DbErrorwith codeUNKNOWNwhenrequireMatchis on (the default), so silent test drift fails loudly.
Assertions run against the record of what was sent:
adapter.raw.queries; // Array<{ text, params, inTransaction }>
inTransaction is true for queries that ran inside a transaction()
callback, which lets you assert that a write happened on a leased
connection. You can also pass capabilities to simulate a transport:
createFixtureDatabase({
capabilities: { interactiveTransactions: false },
});
// now db.transaction() fails before dispatch, exactly like Neon HTTP
What fixtures cannot tell you
- Whether your SQL parses or your schema matches. The fixture returns what you scripted, not what PostgreSQL would do.
- Whether transactions actually commit, roll back, or release connections.
- Type decoding:
bigintas string, timestamp handling, JSON, arrays.
Those need a real database. The repository’s own tests run against a real local Postgres 17 in Docker plus a local Neon HTTP proxy; the commands are in the capabilities page and CONTRIBUTING.
A practical split
- Unit tests: fixture adapter. Fast, deterministic, no credentials, and they pin your SQL text and parameter shapes.
- Integration tests: a real local PostgreSQL via
dbsdk/postgres. Covers binding, decoding, commit/rollback, constraints. - Transport tests: the Neon HTTP adapter against a local proxy, when your code depends on HTTP-specific behavior.
- Hosted checks: against your own Supabase or Neon project if you want them; dbSDK never requires credentials to test, and this project’s own suite runs without any.