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&lt;Env> {
    async fetch(request: Request): Promise&lt;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&lt;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&lt;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"
    }
}

Was this helpful?

YesNo

On this page

Testing Durable Objects·续 - 图1Docs

{"@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


Skip to content

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.