Write tests
Test the database
A database test runs SQL against a local Postgres that Testkit builds from your schema. Use it for constraints, queries, and anything else the database decides.
On this page
Before you start
Start Docker. Testkit runs Postgres in a container. It runs psql and pg_dump in a container too, so you do not need Postgres on the host.
Declare the database
Add database.postgres(...) to the service that owns the schema.
import { app, database, defineConfig } from "@gfdelarue/testkit/config";
export default defineConfig({
services: {
api: app.node({
build: null,
start: "npm start",
port: 4392,
database: database.postgres({
template: {
inputs: ["db/schema.sql"],
migrate: [{ kind: "sql-file", path: "db/schema.sql" }],
},
}),
}),
},
});CREATE TABLE refunds (
id text PRIMARY KEY,
order_id text NOT NULL UNIQUE,
amount_cents integer NOT NULL
);Testkit builds a template database from the migrate steps and caches it. It rebuilds the template when a file in inputs changes.
The service process receives its database as DATABASE_URL.
Write the test
import { defineFile } from "@gfdelarue/testkit";
import { defineDalSuite, expect } from "@gfdelarue/testkit/k6";
export const testkit = defineFile({ engine: "k6" });
const suite = defineDalSuite(({ db }) => {
db.exec("INSERT INTO refunds (id, order_id, amount_cents) VALUES ('r1', '8841', 1200)");
const error = expect.captureError(() => {
db.exec("INSERT INTO refunds (id, order_id, amount_cents) VALUES ('r2', '8841', 1200)");
});
expect.error.captured(
error,
(caught) => String(caught).includes("refunds_order_id_key"),
"the database rejects a second refund",
);
const rows = db.query("SELECT id FROM refunds WHERE order_id = '8841'");
expect.rowCount(rows, 1, "one refund is stored");
});
export const options = suite.options;
export const setup = suite.setup;
export default suite.exec;db.exec(sql) runs a statement. db.query(sql) returns the rows.
expect.captureError runs a function and returns what it threw. expect.error.captured takes that error, a predicate, and a label. The predicate must be a function.
The last three lines are required, as in an HTTP test.
Run it
$npx testkit run dal[1/1] PASS api dal src/db/__testkit__/refunds.dal.testkit.ts 0s
Summary
Result: PASSED
Passed: 1
Failed: 0
Skipped: 0
Not run: 0
Files: 1
Duration: 4s
Run ID: 1791890158174-1344714-af8dd695By default, Testkit runs the dal files for one service database one at a time.
Read a failure
When an expect helper fails, the run prints what it expected and what it got.
$npx testkit run dal[1/1] FAIL api dal src/db/__testkit__/refunds.dal.testkit.ts 0s
k6 thresholds failed: checks(rate==1.0)
expected 2 row(s), got 1| Helper | Passes when |
|---|---|
expect.rowCount(rows, n, label) | The query returned n rows |
expect.singleRow(rows, label) | The query returned one row. It returns that row. |
expect.noRows(rows, label) | The query returned no rows |
expect.error.captured(error, predicate, label) | The predicate accepts the captured error |
Seed rows with fixtures
defineDalFixtures gives a file named fixtures. Each fixture seeds its rows once and returns the values the test needs.
import { defineFile } from "@gfdelarue/testkit";
import { defineDalFixtures, defineDalSuite, expect } from "@gfdelarue/testkit/k6";
export const testkit = defineFile({ engine: "k6" });
const fixtures = defineDalFixtures(({ db, fixtureScope }) => ({
refund() {
return fixtureScope.seed("refund", "primary", { amountCents: 1200 }, () => {
const refundId = fixtureScope.id("refund");
const orderId = fixtureScope.id("order");
db.exec(`
INSERT INTO refunds (id, order_id, amount_cents)
VALUES ('${refundId}', '${orderId}', 1200)
`);
return { refundId };
});
},
}));
const suite = defineDalSuite({ fixtures }, ({ db, fixtures }) => {
const refund = fixtures.refund();
const rows = db.query(`SELECT amount_cents FROM refunds WHERE id = '${refund.refundId}'`);
expect.rowCount(rows, 1, "the seeded refund is stored");
});
export const options = suite.options;
export const setup = suite.setup;
export default suite.exec;fixtureScope.seed(kind, key, signature, create) identifies a fixture by its kind and key.
- A second call with the same
kind,key, andsignaturereturns the first call's value. - A second call with a different
signaturefails the file. - Testkit saves the seeded records with the run as a
testkit.dal-fixturesartifact.
fixtureScope also makes values that are stable for a label: id(label), uuid(label), slug(label), email(label), string(label, options), and token(label).
Remove the database
testkit destroy removes the Postgres container of each service. It keeps .testkit/results, so earlier run records stay readable.
$npx testkit destroyDestroyed apiThe next run that needs the database builds it again.