@flower-js/sdk/testing runs an application on Flower's reference engine inside your test process. Calls behave like HTTP calls, with the same checks, receipts and errors, and time moves only when you say so. It needs Node.
Start a test database
import assert from "node:assert/strict";
import test from "node:test";
import { FlowerError } from "@flower-js/sdk";
import { testDatabase } from "@flower-js/sdk/testing";
import app from "./counter.ts";
test("increments, doubles and rejects bad input", async () => {
const db = await testDatabase(app);
assert.equal(db.mutate("counter.increment", { id: "visits", by: 2 }), 4);
assert.deepEqual(db.query("counter.get", "visits"), { count: 2, doubled: 4 });
assert.throws(() => db.mutate("counter.increment", { id: "visits", by: 0 }),
(error) => error instanceof FlowerError && error.failure?.code === "INVALID_ARGUMENT");
});node --test counter.test.tsquery,mutateandcallare synchronous and return the value. They check arguments and access and keep receipts like HTTP calls, and throw aFlowerErrorwith the same status, code andfailure.- Pass options last:
{ credentials, requestId }. Reusing a request ID replays the first result. - Pass the imported module to run it directly, or a path,
testDatabase<typeof app>("counter.ts"), to bundle it and run it in an isolated context as the server does. testDatabase(app, { now, credentials, partitions })sets the starting clock (1,000,000 ms by default), default credentials, and named partitions.
Control time and maintenance
db.now is the server clock that ctx.now() reads. Nothing moves it but you.
import assert from "node:assert/strict";
import test from "node:test";
import { testDatabase } from "@flower-js/sdk/testing";
import invoices from "./invoices.ts";
test("an invoice is ready five seconds after its last update", async () => {
const db = await testDatabase(invoices);
db.mutate("invoice.update", { id: "inv-1", total: 2400 });
db.advance(4_999);
assert.equal(db.query("invoice.get", "inv-1")?.status, "draft");
db.advance(1);
assert.equal(db.query("invoice.get", "inv-1")?.status, "ready");
});db.advance(ms)moves the clock, then runs due maintenance in every partition, like the leader would. It returns the number of committed runs.db.maintain()runs due maintenance without moving the clock: background materialization, for example.- Maintenance never runs on its own, so a test sees exactly the state it arranged.
Drive the real client
db.client is a FlowerClient<typeof app> wired to the test database. query, mutate, call, watch, subscribe and waitUntil all work, live updates included, so worker code runs unchanged:
import assert from "node:assert/strict";
import test from "node:test";
import { testDatabase } from "@flower-js/sdk/testing";
import app from "./reactive-worker.ts";
import { runWorker } from "./reactive-worker-client.ts";
test("a worker keeps the digest current", async () => {
const db = await testDatabase(app);
const stop = new AbortController();
const worker = runWorker(db.client, stop.signal);
db.mutate("document.put", { id: "notes", text: "Hello, garden." });
const { value } = await db.client.waitUntil("document.get", "notes",
(document) => document?.digest?.status === "ready");
assert.equal(value?.text, "Hello, garden.");
stop.abort();
await worker;
});Partitions and transactions
Name partitions up front. Each has its own data, revision and receipts, and they share the clock. Transactions across them commit together or not at all, in-process.
import assert from "node:assert/strict";
import test from "node:test";
import { FlowerError } from "@flower-js/sdk";
import { testDatabase } from "@flower-js/sdk/testing";
import bank from "./bank.ts";
test("transfers move money between partitions atomically", async () => {
const db = await testDatabase(bank, { partitions: ["west", "east"] });
const west = db.partition("west");
west.mutate("credit", { id: "alice", cents: 500 });
assert.deepEqual(db.call("transfer", { from: "alice", to: "bob", cents: 200 }).results, [300, 200]);
assert.throws(() => db.call("transfer", { from: "alice", to: "bob", cents: 900 }),
(error) => error instanceof FlowerError && error.failure?.code === "INSUFFICIENT_FUNDS");
assert.equal(west.mutate("debit", { id: "alice", cents: 300 }), 0); // The failed transfer moved nothing.
});db.partition(name) has query, mutate, call, maintain and its own client. As on a server, if the app has access checks, a principal's tenant must match the partition.
What differs from a server
- No native crypto. NaCl, JWT, managed keys and
jwtBearerfail inside the test database. Test access rules with your ownauthenticatefunction. - Undeclared indexes fail.
ctx.range,ctx.queryandctx.scan({ index })fail withUNDECLARED_INDEXwhen the index isn't declared throughdefine({ collections })or a component, because the server would quietly scan the whole collection. A forgotten declaration shows up in tests instead of as a slowdown in production. - One process, no replication: replica lag, budgets, deployment steps and retry sessions aren't simulated.
- Watches send full snapshots rather than patches.
db.datais a copy of the raw state for debugging. Its format isn't stable.