Test your backend

You need: a scaffolded project. The wiring is already there.

A test that mocks D1 proves the mock works

Live fixtures is the case where the real account is the point, migrations is what a workers test runs first, and CI/CD is where all of it is enforced.

That is the kit’s whole testing argument, and it is why the scaffold ships two test configurations rather than one.

Workers tests run inside the real runtime, against real D1 and real KV through the local emulator. Everything else runs in Node.

The split is by filename, so a test lands in the right runtime by being named for it.

What is scaffolded

pithy init writes both configurations, the bindings the Workers half needs, and a matching ambient type declaration — plus a passing binding test, so the fiddly half is proven before you write anything.

A capability that needs a new binding needs it in both places: the test configuration and the type declaration. That is the one thing to remember.

What to test in which runtime

In the Workers runtime: anything touching a database, a namespace, a Durable Object or a binding. Repository functions, migrations, route handlers with real storage behind them.

In Node: pure logic. Codecs. Schema validation. Anything with no platform in it.

When in doubt, the Workers runtime is the safer answer — it is slower and it is correct.

Testing a route

Call the handler with a real request against a real database. No mock, no fixture layer, no in-memory substitute:

const res = await app.request("/notes", { headers: { authorization: `Bearer ${token}` } });
expect(res.status).toBe(200);

The tables are there because the migrations ran. The migrations are the same ones production runs.

Test the rollback

Every migration’s down is tested, and that is the kit’s own standard rather than a suggestion.

The reason is that a rollback is the one piece of code you need under pressure and have never run. pithy migrate --rollback steps the latest one back, which is what makes exercising it cheap.

Every codec round-trips in tests for the same reason: a conversion that is wrong in one direction is a data loss that shows up months later.

Where a Workflow is hard, and what to do

A durable job is genuinely awkward to test end to end, because the interesting behavior is in the retries and the checkpointing.

Test the step, not the Workflow. A step is a function; hand it what the runtime would and assert what it returns. The orchestration is the platform’s, and testing the platform is not your job.

Then prove the wiring once — that the binding exists and the job dispatches — rather than re-proving the runtime on every job.

Idempotency deserves its own test

Anything carrying an idempotency reference — a ledger movement, a purchase projection, a seeded row — has a test worth writing: do it twice, assert the state is what it was after once.

That is the property the whole design rests on, and it is cheap to assert and expensive to discover missing.

The gates a scaffold ships with

Three, and CI needs all three:

ScriptRuns
typecheckThe whole solution
testBoth runtimes
lintThe formatter and the two lint plugins

The root TypeScript config is a solution file, and it has to be. A Worker program needs the Workers types and a browser program needs the DOM, and a program carrying both makes a byte array structurally incompatible with a buffer source — which breaks every crypto call in the kit’s own signing code. Keeping the two type worlds apart is the only arrangement that compiles.

One gap, stated rather than hidden: pithy worker add does not add the new Worker to the solution file. Add the reference yourself, or that Worker’s source is typechecked by nothing.

Add pithy doctor to CI

pithy doctor --json

It exits non-zero when any Worker fails a check, and it catches things a unit test structurally cannot: a binding declared and not bound, a Workflow declared and never synced, a domain nothing routes, a capability composed without its prerequisites, a locale with gaps.

The unsynced Workflow is the one worth having. Its entire symptom is that nothing happens — no request fails, no log line appears, no probe goes red, and the cron simply never fires.

And the asset-routing check, if you have a front end

pithy ui sync --check --worker api

A shadowed route answers a 200 of the SPA shell with your handler never invoked — and no test suite of yours sees it, because tests call handlers directly and the asset router is never in the picture.

That is exactly the shape of bug that needs a check rather than a test.

ESC