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.

Pure functions get ordinary unit tests. Database behavior can’t. A mocked env.DB happily accepts queries your real schema would reject. So database code deserves tests that run in the Workers runtime, with a real D1 binding and your actual migration history.

Cloudflare’s Vitest integration, @cloudflare/vitest-pool-workers, runs your tests inside the same runtime your Worker uses, with real bindings. You import the environment and clean the tables before every test:

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.

Be careful with tests that pass only because your old local database already has 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 the 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/)
})

Here is my list for the notes app:

  • a duplicate email hitting the UNIQUE constraint
  • a note whose owner does not exist
  • a batch where the second statement fails, so the first one must roll back
  • pagination boundaries: empty page, exact page size, past the end
  • an update on someone else’s note, where meta.changes must be zero
  • the same idempotency key sent twice, ending with 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 the local state and prove the suite recreates everything from the 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.

Try this now: write the failure-path suite for the notes schema, then run the wipe-and-rerun check above. I’d make it part of how you work, not a one-time exercise. It’s the cheapest way to find out your migrations are incomplete.

Lesson completed