Test, back up, and operate D1
Test the real schema and failure paths
Run integration tests against a clean D1 binding created from migrations and cover constraints, authorization, and retries.
8 minute lesson
Pure functions can use ordinary unit tests. Database behavior cannot — a mocked env.DB happily accepts queries your real schema would reject. Database behavior deserves tests in the Workers runtime with a D1 binding and the actual migration history.
Cloudflare’s Vitest integration (@cloudflare/vitest-pool-workers) runs your tests inside the same runtime your Worker uses, with real bindings. Tests import the environment and apply your committed migrations before anything else runs:
import { env } from 'cloudflare:test'
import { beforeEach, expect, it } from 'vitest'
beforeEach(async () => {
await env.DB.exec('delete from audit_log')
await env.DB.exec('delete from notes')
})
Build fresh state per test. Each test seeds exactly the rows it needs and assumes nothing about leftovers. Avoid tests that pass only because a developer’s old local database already contains the schema — that suite fails on every new machine and in CI, and nobody remembers why.
Test the paths that fail
The happy path is one test. The failure paths are where the schema and authorization work earn their keep:
it('rejects a note with a missing owner', async () => {
const attempt = env.DB.prepare(
'insert into notes (user_id, title, created_at) values (?, ?, ?)'
).bind(9999, 'orphan', Date.now()).run()
await expect(attempt).rejects.toThrow(/FOREIGN KEY constraint failed/)
})
Cover the full list: duplicate values hitting a UNIQUE constraint, missing foreign rows, a transaction failure that must roll back its earlier statements, pagination boundaries (empty page, exact page size, past the end), unauthorized updates where meta.changes must be zero, and an idempotent retry that runs the same logical operation twice and asserts one row.
Each of these is a production incident you are choosing to have in a test instead.
Prove the suite owns its schema
The final check is reproducibility. Delete local test state and prove the suite recreates everything from committed migrations:
rm -rf .wrangler/state
npm test
# ✓ all tests pass on a database built from migrations alone
If tests fail after this, they depended on state that only existed on your machine. Fix the setup, not the assertion.
Now write the failure-path suite for the notes schema, then run the wipe-and-rerun check above and make it part of how you work.
Lesson completed