Testing Durable Objects·续
// Check database size is non-zero expect(state.storage.sql.databaseSize).toBeGreaterThan(0); }); }); });
*test/sqlite.test.tsts*
```ts
import { env } from "cloudflare:workers";
import { runInDurableObject } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("SQLite in Durable Objects", () => {
it("can query and verify SQLite storage", async () => {
const id = env.COUNTER.idFromName("sqlite-test");
const stub = env.COUNTER.get(id);
// Increment the counter a few times via RPC
await stub.increment("page-views");
await stub.increment("page-views");
await stub.increment("api-calls");
// Verify the data directly in SQLite
await runInDurableObject(stub, async (instance, state) => {
// Query the database directly
const rows = state.storage.sql
.exec<{ name: string; value: number }>("SELECT name, value FROM counters ORDER BY name")
.toArray();
expect(rows).toEqual([
{ name: "api-calls", value: 1 },
{ name: "page-views", value: 2 },
]);
// Check database size is non-zero
expect(state.storage.sql.databaseSize).toBeGreaterThan(0);
});
});
});
Testing alarms
Use runDurableObjectAlarm() to immediately trigger a scheduled alarm without waiting for the timer. This allows you to test alarm handlers synchronously:
test/alarm.test.jsjs
import { env } from "cloudflare:workers";
import { runInDurableObject, runDurableObjectAlarm } from "cloudflare:test";
import { describe, it, expect } from "vitest";
import { Counter } from "../src";
describe("Durable Object alarms", () => {
it("can trigger alarms immediately", async () => {
const id = env.COUNTER.idFromName("alarm-test");
const stub = env.COUNTER.get(id);
// Increment counter and schedule a reset alarm
await stub.increment();
await stub.increment();
expect(await stub.getCount()).toBe(2);
// Schedule an alarm (in a real app, this might be hours in the future)
await runInDurableObject(stub, async (instance, state) => {
await state.storage.setAlarm(Date.now() + 60_000); // 1 minute from now
});
// Immediately execute the alarm without waiting
const alarmRan = await runDurableObjectAlarm(stub);
expect(alarmRan).toBe(true); // Alarm was scheduled and executed
// Verify the alarm handler ran (assuming it resets the counter)
// Note: You'll need an alarm() method in your Durable Object that handles resets
// expect(await stub.getCount()).toBe(0);
// Trying to run the alarm again returns false (no alarm scheduled)
const alarmRanAgain = await runDurableObjectAlarm(stub);
expect(alarmRanAgain).toBe(false);
});
});
test/alarm.test.tsts
import { env } from "cloudflare:workers";
import {
runInDurableObject,
runDurableObjectAlarm,
} from "cloudflare:test";
import { describe, it, expect } from "vitest";
import { Counter } from "../src";
describe("Durable Object alarms", () => {
it("can trigger alarms immediately", async () => {
const id = env.COUNTER.idFromName("alarm-test");
const stub = env.COUNTER.get(id);
// Increment counter and schedule a reset alarm
await stub.increment();
await stub.increment();
expect(await stub.getCount()).toBe(2);
// Schedule an alarm (in a real app, this might be hours in the future)
await runInDurableObject(stub, async (instance, state) => {
await state.storage.setAlarm(Date.now() + 60_000); // 1 minute from now
});
// Immediately execute the alarm without waiting
const alarmRan = await runDurableObjectAlarm(stub);
expect(alarmRan).toBe(true); // Alarm was scheduled and executed
// Verify the alarm handler ran (assuming it resets the counter)
// Note: You'll need an alarm() method in your Durable Object that handles resets
// expect(await stub.getCount()).toBe(0);
// Trying to run the alarm again returns false (no alarm scheduled)
const alarmRanAgain = await runDurableObjectAlarm(stub);
expect(alarmRanAgain).toBe(false);
});
});
To test alarms, add an alarm() method to your Durable Object:
src/index.jsjs
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject {
// ... other methods ...
async alarm() {
// This method is called when the alarm fires
// Reset all counters
this.ctx.storage.sql.exec("DELETE FROM counters");
}
async scheduleReset(afterMs) {
await this.ctx.storage.setAlarm(Date.now() + afterMs);
}
}
src/index.tsts
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject {
// ... other methods ...
async alarm() {
// This method is called when the alarm fires
// Reset all counters
this.ctx.storage.sql.exec("DELETE FROM counters");
}
async scheduleReset(afterMs: number) {
await this.ctx.storage.setAlarm(Date.now() + afterMs);
}
}
Testing eviction
Use evictDurableObject() to evict a Durable Object instance during tests. Eviction tears down the instance to reset its in-memory state. This lets you test how your Durable Object recovers state from storage after being evicted.
By default, hibernatable WebSockets are hibernated rather than closed, and eviction waits up to 30 seconds for in-flight requests to drain before tearing down the instance.
The following test sets both in-memory state (cachedHits) and durable storage (the counter value), evicts the Durable Object, and verifies that the in-memory state is wiped while the stored count survives:
test/eviction.test.jsjs
import { env } from "cloudflare:workers";
import { evictDurableObject } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("Durable Object eviction", () => {
it("wipes in-memory state but preserves storage across eviction", async () => {
const id = env.COUNTER.idFromName("evict-test");
const stub = env.COUNTER.get(id);
// Persist a value to SQLite storage
await stub.increment();
await stub.increment();
expect(await stub.getCount()).toBe(2);
// Set in-memory only state, which is not persisted to storage
await stub.recordHit();
await stub.recordHit();
expect(await stub.getHits()).toBe(2);
// Evict the Durable Object. The in-memory instance is torn down,
// but durable storage is preserved.
await evictDurableObject(stub);
// In-memory state is wiped: the reconstructed instance starts fresh
expect(await stub.getHits()).toBe(0);
// Durable storage survives: the persisted count is read back
expect(await stub.getCount()).toBe(2);
});
});
test/eviction.test.tsts
import { env } from "cloudflare:workers";
import { evictDurableObject } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("Durable Object eviction", () => {
it("wipes in-memory state but preserves storage across eviction", async () => {
const id = env.COUNTER.idFromName("evict-test");
const stub = env.COUNTER.get(id);
// Persist a value to SQLite storage
await stub.increment();
await stub.increment();
expect(await stub.getCount()).toBe(2);
// Set in-memory only state, which is not persisted to storage
await stub.recordHit();
await stub.recordHit();
expect(await stub.getHits()).toBe(2);
// Evict the Durable Object. The in-memory instance is torn down,
// but durable storage is preserved.
await evictDurableObject(stub);
// In-memory state is wiped: the reconstructed instance starts fresh
expect(await stub.getHits()).toBe(0);
// Durable storage survives: the persisted count is read back
expect(await stub.getCount()).toBe(2);
});
});
Testing WebSocket behavior across eviction
You can control what happens to hibernatable WebSockets when a Durable Object is evicted by passing the options parameter:
{ webSockets: "hibernate" }(the default) hibernates WebSockets so they can resume after eviction.{ webSockets: "close" }closes WebSockets during eviction.
The following example uses a Durable Object that accepts WebSocket connections with the hibernatable WebSockets API:
src/websocket-server.jsjs
import { DurableObject } from "cloudflare:workers";
export class WebSocketServer extends DurableObject {
async fetch(request) {
const [client, server] = Object.values(new WebSocketPair());
// Accept the WebSocket as hibernatable so it can survive eviction
this.ctx.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client });
}
webSocketMessage(ws, message) {
// Echo the received message back to the client
ws.send(message);
}
webSocketClose(ws, code, reason, wasClean) {
// Handle WebSocket close events
}
}
src/websocket-server.tsts
import { DurableObject } from "cloudflare:workers";
export class WebSocketServer extends DurableObject<Env> {
async fetch(request: Request): Promise<Response> {
const [client, server] = Object.values(new WebSocketPair());
// Accept the WebSocket as hibernatable so it can survive eviction
this.ctx.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client });
}
webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
// Echo the received message back to the client
ws.send(message);
}
webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {
// Handle WebSocket close events
}
}
Add a binding and migration for the Durable Object in your Wrangler configuration, alongside the existing COUNTER binding:
{
"durable_objects": {
"bindings": [
{ "name": "WEBSOCKET_SERVER", "class_name": "WebSocketServer" }
]
},
"migrations": [
{ "tag": "v2", "new_sqlite_classes": ["WebSocketServer"] }
]
}
[[durable_objects.bindings]]
name = "WEBSOCKET_SERVER"
class_name = "WebSocketServer"
[[migrations]]
tag = "v2"
new_sqlite_classes = [ "WebSocketServer" ]
With the default options, hibernatable WebSockets remain open across eviction, so messages still round-trip afterwards. Passing { webSockets: "close" } closes them instead:
test/eviction-websockets.test.jsjs
import { env } from "cloudflare:workers";
import { evictDurableObject } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("WebSocket eviction behavior", () => {
it("hibernates WebSockets across eviction by default", async () => {
const id = env.WEBSOCKET_SERVER.idFromName("ws-test");
const stub = env.WEBSOCKET_SERVER.get(id);
const response = await stub.fetch("https://example.com", {
headers: { Upgrade: "websocket" },
});
const socket = response.webSocket;
if (!socket) throw new Error("Expected WebSocket response");
socket.accept();
// Hibernatable WebSockets are hibernated, not closed
await evictDurableObject(stub);
// Messages still round-trip after eviction wakes the Durable Object
const message = new Promise((resolve) => {
socket.addEventListener("message", (event) => {
resolve(event.data);
});
});
socket.send("after-eviction");
expect(await message).toBe("after-eviction");
socket.close(1000, "done");
});
it("closes WebSockets when requested", async () => {
const id = env.WEBSOCKET_SERVER.idFromName("ws-close-test");
const stub = env.WEBSOCKET_SERVER.get(id);
const response = await stub.fetch("https://example.com", {
headers: { Upgrade: "websocket" },
});
const socket = response.webSocket;
if (!socket) throw new Error("Expected WebSocket response");
socket.accept();
const closed = new Promise((resolve) => {
socket.addEventListener("close", (event) => resolve(event));
});
// Close WebSockets instead of hibernating them
await evictDurableObject(stub, { webSockets: "close" });
expect(await closed).toBeDefined();
});
});
test/eviction-websockets.test.tsts
import { env } from "cloudflare:workers";
import { evictDurableObject } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("WebSocket eviction behavior", () => {
it("hibernates WebSockets across eviction by default", async () => {
const id = env.WEBSOCKET_SERVER.idFromName("ws-test");
const stub = env.WEBSOCKET_SERVER.get(id);
const response = await stub.fetch("https://example.com", {
headers: { Upgrade: "websocket" },
});
const socket = response.webSocket;
if (!socket) throw new Error("Expected WebSocket response");
socket.accept();
// Hibernatable WebSockets are hibernated, not closed
await evictDurableObject(stub);
// Messages still round-trip after eviction wakes the Durable Object
const message = new Promise<string>((resolve) => {
socket.addEventListener("message", (event) => {
resolve(event.data as string);
});
});
socket.send("after-eviction");
expect(await message).toBe("after-eviction");
socket.close(1000, "done");
});
it("closes WebSockets when requested", async () => {
const id = env.WEBSOCKET_SERVER.idFromName("ws-close-test");
const stub = env.WEBSOCKET_SERVER.get(id);
const response = await stub.fetch("https://example.com", {
headers: { Upgrade: "websocket" },
});
const socket = response.webSocket;
if (!socket) throw new Error("Expected WebSocket response");
socket.accept();
const closed = new Promise<CloseEvent>((resolve) => {
socket.addEventListener("close", (event) => resolve(event));
});
// Close WebSockets instead of hibernating them
await evictDurableObject(stub, { webSockets: "close" });
expect(await closed).toBeDefined();
});
});
To evict all currently-running Durable Objects at once (for example, to reset state between tests without deleting persisted data), use evictAllDurableObjects():
import { evictAllDurableObjects } from "cloudflare:test";
import { afterEach } from "vitest";
afterEach(async () => {
await evictAllDurableObjects();
});
For more details on the eviction helpers, including the DurableObjectEvictionOptions interface, refer to the Test APIs reference.
Running tests
Run your tests with:
npx vitest
Or add a script to your package.json:
{
"scripts": {
"test": "vitest"
}
}
Related resources
- Workers Vitest integration - Full documentation for the Vitest integration
- Durable Objects testing recipe ↗︎ - Example from the Workers SDK
- RPC testing recipe ↗︎ - Testing JSRPC with Durable Objects
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/durable-objects/examples/testing-with-durable-objects/#page","headline":"Testing Durable Objects","description":"Write tests for Durable Objects using the Workers Vitest integration.","url":"https://developers.cloudflare.com/durable-objects/examples/testing-with-durable-objects/","inLanguage":"en","image":"https://developers.cloudflare.com/durable-objects/examples/testing-with-durable-objects/og.png?v=1e1b96c330a686da","dateModified":"2026-10-12","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Read and write to/from KV within a Durable Object title: Use Workers KV from Durable Objects image: https://developers.cloudflare.com/durable-objects/examples/use-kv-from-durable-objects/og.png?v=8ee05704f188758a
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/durable-objects/llms.txt
Use this file to discover all available pages before exploring further.
Docs