Testing Durable Objects for real
Cloudflare has great docs on building with Durable Objects. The testing story
gets less attention, but it's surprisingly good. @cloudflare/vitest-pool-workers
runs your tests inside the actual Workers runtime: real DOs, real SQLite storage,
real alarms. No mocking the platform.
Two configs, two worlds
The trick is splitting tests into two vitest configs. Unit tests run in the normal Node pool. They're fast. You mock external APIs (Tavily, Gemini, Resend) and test pure logic like parsing, validation, and query building.
Integration tests use @cloudflare/vitest-pool-workers.
These run inside workerd, the actual Workers runtime. Your Durable Objects
get real embedded SQLite, real storage.setAlarm(),
real WebSocketPair. The test file imports
env from cloudflare:test and gets the same
bindings your wrangler config declares.
What a real test looks like
Here's a simplified test from web-explorer. It creates a Durable Object, starts it, triggers an alarm, and checks that a card was generated:
import { env, runDurableObjectAlarm } from "cloudflare:test";
const id = env.EXPLORATION_DO.newUniqueId();
const stub = env.EXPLORATION_DO.get(id);
// Start the exploration (arms the first alarm)
await stub.start("2026-03-26");
// Fire the alarm — runs one step
await runDurableObjectAlarm(stub);
const data = await stub.getExploration();
expect(data.cards).toHaveLength(1);
expect(data.status).toBe("generating");
That's a real Durable Object writing to real SQLite storage. The alarm fires
one step of the exploration loop, stores the result, and the test reads it
back. External APIs (LLM, search) are mocked with vi.mock() so
no keys are needed. The platform itself is real.
The gotcha
vitest-pool-workers has an isolatedStorage
option that resets DO storage between tests. Sounds great, but it conflicts
with storage.setAlarm(). The alarm write happens inside the DO,
and the isolation mechanism doesn't expect it.
The fix: set isolatedStorage: false and use unique DO IDs per test.
Each test calls env.MY_DO.newUniqueId() to get a fresh instance.
No shared state, no conflicts.
The split in practice
Across two projects (web-explorer and inboxkit), we have 150+ tests. Unit tests cover API clients, parsing, validation, crypto. Integration tests cover DO lifecycle: creation, alarm progression, WebSocket connections, RPC calls, error recovery. CI runs both in sequence. The whole suite finishes in under 10 seconds.
The setup takes 20 lines of config. After that, testing Durable Objects feels like testing any other code.
web-explorer (104 tests)