Production and deployment
Test with the Workers runtime
Run integration tests inside the Workers execution environment with local bindings instead of mocking every platform API.
Mocking D1, KV, R2, and Queues one by one is a lot of work, and the mocks drift from the real APIs. Cloudflare’s answer is the Vitest integration: your tests run inside the actual Workers runtime, with real local bindings. The project generator already set it up in vitest.config.mts.
Two kinds of tests
Keep them separate, because they prove different things.
Pure unit tests cover validation, the problem-response helpers, and state transitions. They import plain modules and never touch env. They are fast, and you can have hundreds.
Runtime integration tests cover the exported Worker, the generated Env, the migrations, and the binding calls working together. They import from cloudflare:test.
A runtime test
SELF is your Worker. env is the same environment your handler gets:
import { env, SELF } from 'cloudflare:test'
import { expect, it } from 'vitest'
it('creates and lists a link', async () => {
const created = await SELF.fetch('http://localhost/api/links', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ url: 'https://flaviocopes.com', title: 'Flavio' })
})
expect(created.status).toBe(201)
const list = await SELF.fetch('http://localhost/api/links')
const { results } = await list.json()
expect(results).toHaveLength(1)
})
The request goes through Hono, hits the real D1 adapter, and writes to a local database. The assertion checks the effect, not a mock’s call count.
Apply migrations in setup
An empty local D1 has no links table. The integration ships readD1Migrations and applyD1Migrations for this. Read the migrations in vitest.config.mts, pass them through a test binding, and apply them in a setup file. The d1 example in Cloudflare’s repo shows the wiring.
Reset between tests
The integration isolates storage per test by default. Every it starts with the state the setup file left. Keep it that way. A test that relies on a row another test inserted breaks the day someone reorders the file.
Test the failures too
Happy paths are the easy part. Add a test for each of these:
- a request before migrations run, so the error message stays useful
- the same queue message delivered twice, so the consumer writes one object
- a
GETfor an R2 key that doesn’t exist, so it’s a404and not a crash - a KV cache miss followed by a hit
Don’t try to reproduce KV’s global propagation delay locally. The emulator can’t, and you’d be testing the emulator. Test that your code stays correct when KV returns a stale or missing value.
Keep one remote smoke test
Local tests prove the code. They don’t prove the deployed version points at the right remote database. After each deployment, run one curl against the health route and one against a route that touches each binding. That’s your smoke test, and we come back to it in the deployment lesson.
Now write four runtime tests for Link Vault: create and list in D1, a KV miss then hit, an R2 export written and streamed back, and duplicate queue processing that leaves exactly one object.
Lesson completed