使用 @cloudflare/vitest-pool-workers ↗ 包为 Durable Objects 编写测试。此集成在 Workers 运行时内运行测试,使您能够直接访问 Durable Object 绑定(binding)和 API。
将 Vitest 和 Workers Vitest 集成安装为开发依赖:
npm i -D vitest@^4.1.0 @cloudflare/vitest-pool-workerspnpm add -D vitest@^4.1.0 @cloudflare/vitest-pool-workersyarn add -D vitest@^4.1.0 @cloudflare/vitest-pool-workers此示例测试带有 SQLite 存储的简单计数器 Durable Object:
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS counters (
name TEXT PRIMARY KEY,
value INTEGER NOT NULL DEFAULT 0
)
`);
});
}
// In-memory only. This field lives on the instance and is not persisted
// to storage, so it is reset whenever the Durable Object is evicted and
// reconstructed.
cachedHits = 0;
recordHit() {
return ++this.cachedHits;
}
getHits() {
return this.cachedHits;
}
async increment(name = "default") {
this.ctx.storage.sql.exec(
`INSERT INTO counters (name, value) VALUES (?, 1)
ON CONFLICT(name) DO UPDATE SET value = value + 1`,
name,
);
const result = this.ctx.storage.sql
.exec("SELECT value FROM counters WHERE name = ?", name)
.one();
return result.value;
}
async getCount(name = "default") {
const result = this.ctx.storage.sql
.exec("SELECT value FROM counters WHERE name = ?", name)
.toArray();
return result[0]?.value ?? 0;
}
async reset(name = "default") {
this.ctx.storage.sql.exec("DELETE FROM counters WHERE name = ?", name);
}
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
const counterId = url.searchParams.get("id") ?? "default";
const id = env.COUNTER.idFromName(counterId);
const stub = env.COUNTER.get(id);
if (request.method === "POST") {
const count = await stub.increment();
return Response.json({ count });
}
const count = await stub.getCount();
return Response.json({ count });
},
};import { DurableObject } from "cloudflare:workers";
export interface Env {
COUNTER: DurableObjectNamespace<Counter>;
}
export class Counter extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS counters (
name TEXT PRIMARY KEY,
value INTEGER NOT NULL DEFAULT 0
)
`);
});
}
// In-memory only. This field lives on the instance and is not persisted
// to storage, so it is reset whenever the Durable Object is evicted and
// reconstructed.
cachedHits = 0;
recordHit(): number {
return ++this.cachedHits;
}
getHits(): number {
return this.cachedHits;
}
async increment(name: string = "default"): Promise<number> {
this.ctx.storage.sql.exec(
`INSERT INTO counters (name, value) VALUES (?, 1)
ON CONFLICT(name) DO UPDATE SET value = value + 1`,
name
);
const result = this.ctx.storage.sql
.exec<{ value: number }>("SELECT value FROM counters WHERE name = ?", name)
.one();
return result.value;
}
async getCount(name: string = "default"): Promise<number> {
const result = this.ctx.storage.sql
.exec<{ value: number }>("SELECT value FROM counters WHERE name = ?", name)
.toArray();
return result[0]?.value ?? 0;
}
async reset(name: string = "default"): Promise<void> {
this.ctx.storage.sql.exec("DELETE FROM counters WHERE name = ?", name);
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const counterId = url.searchParams.get("id") ?? "default";
const id = env.COUNTER.idFromName(counterId);
const stub = env.COUNTER.get(id);
if (request.method === "POST") {
const count = await stub.increment();
return Response.json({ count });
}
const count = await stub.getCount();
return Response.json({ count });
},
};创建使用 cloudflareTest() 插件的 vitest.config.ts 文件:
import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [
cloudflareTest({
wrangler: { configPath: "./wrangler.jsonc" },
}),
],
});确保 Wrangler 配置包含 Durable Object 绑定(binding)和 SQLite 迁移:
{
"name": "counter-worker",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-17",
"durable_objects": {
"bindings": [
{ "name": "COUNTER", "class_name": "Counter" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Counter"] }
]
}name = "counter-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-17"
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "Counter" ]创建 test/tsconfig.json 以配置测试的 TypeScript:
{
"extends": "../tsconfig.json",
"compilerOptions": {
"moduleResolution": "bundler",
"types": ["@cloudflare/vitest-pool-workers/types"]
},
"include": ["./**/*.ts", "../src/worker-configuration.d.ts"]
}创建 env.d.ts 文件以键入测试环境:
declare module "cloudflare:workers" {
interface ProvidedEnv extends Env {}
}您可以直接从 cloudflare:workers 提供的 env 对象获取 Durable Object stub:
import { env } from "cloudflare:workers";
import { describe, it, expect, beforeEach } from "vitest";
describe("Counter Durable Object", () => {
it("should increment the counter", async () => {
const id = env.COUNTER.idFromName("test-counter");
const stub = env.COUNTER.get(id);
// Call RPC methods directly on the stub
const count1 = await stub.increment();
expect(count1).toBe(1);
const count2 = await stub.increment();
expect(count2).toBe(2);
const count3 = await stub.increment();
expect(count3).toBe(3);
});
it("should persist storage within a test file", async () => {
const id = env.COUNTER.idFromName("test-counter");
const stub = env.COUNTER.get(id);
expect(await stub.getCount()).toBe(3);
});
it("should reset a counter", async () => {
const id = env.COUNTER.idFromName("test-counter");
const stub = env.COUNTER.get(id);
await stub.increment("my-counter");
await stub.increment("my-counter");
expect(await stub.getCount("my-counter")).toBe(2);
await stub.reset("my-counter");
expect(await stub.getCount("my-counter")).toBe(0);
});
it("should isolate different Durable Object instances", async () => {
const id1 = env.COUNTER.idFromName("counter-1");
const id2 = env.COUNTER.idFromName("counter-2");
const stub1 = env.COUNTER.get(id1);
const stub2 = env.COUNTER.get(id2);
await stub1.increment();
await stub1.increment();
await stub2.increment();
// Each Durable Object instance has its own storage
expect(await stub1.getCount()).toBe(2);
expect(await stub2.getCount()).toBe(1);
});
});import { env } from "cloudflare:workers";
import { describe, it, expect, beforeEach } from "vitest";
describe("Counter Durable Object", () => {
it("should increment the counter", async () => {
const id = env.COUNTER.idFromName("test-counter");
const stub = env.COUNTER.get(id);
// Call RPC methods directly on the stub
const count1 = await stub.increment();
expect(count1).toBe(1);
const count2 = await stub.increment();
expect(count2).toBe(2);
const count3 = await stub.increment();
expect(count3).toBe(3);
});
it("should persist storage within a test file", async () => {
const id = env.COUNTER.idFromName("test-counter");
const stub = env.COUNTER.get(id);
expect(await stub.getCount()).toBe(3);
});
it("should reset a counter", async () => {
const id = env.COUNTER.idFromName("test-counter");
const stub = env.COUNTER.get(id);
await stub.increment("my-counter");
await stub.increment("my-counter");
expect(await stub.getCount("my-counter")).toBe(2);
await stub.reset("my-counter");
expect(await stub.getCount("my-counter")).toBe(0);
});
it("should isolate different Durable Object instances", async () => {
const id1 = env.COUNTER.idFromName("counter-1");
const id2 = env.COUNTER.idFromName("counter-2");
const stub1 = env.COUNTER.get(id1);
const stub2 = env.COUNTER.get(id2);
await stub1.increment();
await stub1.increment();
await stub2.increment();
// Each Durable Object instance has its own storage
expect(await stub1.getCount()).toBe(2);
expect(await stub2.getCount()).toBe(1);
});
});使用 exports.default.fetch() 测试 Worker 的 HTTP 处理程序,该处理程序将请求路由到 Durable Objects:
import { exports } from "cloudflare:workers";
import { describe, it, expect } from "vitest";
describe("Counter Worker integration", () => {
it("should increment via HTTP POST", async () => {
const response = await exports.default.fetch(
"http://example.com?id=http-test",
{
method: "POST",
},
);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.count).toBe(1);
});
it("should get count via HTTP GET", async () => {
// First increment the counter
await exports.default.fetch("http://example.com?id=get-test", {
method: "POST",
});
await exports.default.fetch("http://example.com?id=get-test", {
method: "POST",
});
// Then get the count
const response = await exports.default.fetch(
"http://example.com?id=get-test",
);
const data = await response.json();
expect(data.count).toBe(2);
});
it("should use different counters for different IDs", async () => {
await exports.default.fetch("http://example.com?id=counter-a", {
method: "POST",
});
await exports.default.fetch("http://example.com?id=counter-a", {
method: "POST",
});
await exports.default.fetch("http://example.com?id=counter-b", {
method: "POST",
});
const responseA = await exports.default.fetch(
"http://example.com?id=counter-a",
);
const responseB = await exports.default.fetch(
"http://example.com?id=counter-b",
);
const dataA = await responseA.json();
const dataB = await responseB.json();
expect(dataA.count).toBe(2);
expect(dataB.count).toBe(1);
});
});import { exports } from "cloudflare:workers";
import { describe, it, expect } from "vitest";
describe("Counter Worker integration", () => {
it("should increment via HTTP POST", async () => {
const response = await exports.default.fetch("http://example.com?id=http-test", {
method: "POST",
});
expect(response.status).toBe(200);
const data = await response.json<{ count: number }>();
expect(data.count).toBe(1);
});
it("should get count via HTTP GET", async () => {
// First increment the counter
await exports.default.fetch("http://example.com?id=get-test", { method: "POST" });
await exports.default.fetch("http://example.com?id=get-test", { method: "POST" });
// Then get the count
const response = await exports.default.fetch("http://example.com?id=get-test");
const data = await response.json<{ count: number }>();
expect(data.count).toBe(2);
});
it("should use different counters for different IDs", async () => {
await exports.default.fetch("http://example.com?id=counter-a", { method: "POST" });
await exports.default.fetch("http://example.com?id=counter-a", { method: "POST" });
await exports.default.fetch("http://example.com?id=counter-b", { method: "POST" });
const responseA = await exports.default.fetch("http://example.com?id=counter-a");
const responseB = await exports.default.fetch("http://example.com?id=counter-b");
const dataA = await responseA.json<{ count: number }>();
const dataB = await responseB.json<{ count: number }>();
expect(dataA.count).toBe(2);
expect(dataB.count).toBe(1);
});
});使用 runInDurableObject() 直接访问实例属性和存储。这对于验证内部状态或测试私有方法很有用:
import { env } from "cloudflare:workers";
import { runInDurableObject, listDurableObjectIds } from "cloudflare:test";
import { describe, it, expect } from "vitest";
import { Counter } from "../src";
describe("Direct Durable Object access", () => {
it("can access instance internals and storage", async () => {
const id = env.COUNTER.idFromName("direct-test");
const stub = env.COUNTER.get(id);
// First, interact normally via RPC
await stub.increment();
await stub.increment();
// Then use runInDurableObject to inspect internals
await runInDurableObject(stub, async (instance, state) => {
// Access the exact same class instance
expect(instance).toBeInstanceOf(Counter);
// Access storage directly for verification
const result = state.storage.sql
.exec("SELECT value FROM counters WHERE name = ?", "default")
.one();
expect(result.value).toBe(2);
});
});
it("can list all Durable Object IDs in a namespace", async () => {
// Create some Durable Objects
const id1 = env.COUNTER.idFromName("list-test-1");
const id2 = env.COUNTER.idFromName("list-test-2");
await env.COUNTER.get(id1).increment();
await env.COUNTER.get(id2).increment();
// List all IDs in the namespace
const ids = await listDurableObjectIds(env.COUNTER);
expect(ids.length).toBeGreaterThanOrEqual(2);
expect(ids.some((id) => id.equals(id1))).toBe(true);
expect(ids.some((id) => id.equals(id2))).toBe(true);
});
});import { env } from "cloudflare:workers";
import {
runInDurableObject,
listDurableObjectIds,
} from "cloudflare:test";
import { describe, it, expect } from "vitest";
import { Counter } from "../src";
describe("Direct Durable Object access", () => {
it("can access instance internals and storage", async () => {
const id = env.COUNTER.idFromName("direct-test");
const stub = env.COUNTER.get(id);
// First, interact normally via RPC
await stub.increment();
await stub.increment();
// Then use runInDurableObject to inspect internals
await runInDurableObject(stub, async (instance: Counter, state) => {
// Access the exact same class instance
expect(instance).toBeInstanceOf(Counter);
// Access storage directly for verification
const result = state.storage.sql
.exec<{ value: number }>(
"SELECT value FROM counters WHERE name = ?",
"default"
)
.one();
expect(result.value).toBe(2);
});
});
it("can list all Durable Object IDs in a namespace", async () => {
// Create some Durable Objects
const id1 = env.COUNTER.idFromName("list-test-1");
const id2 = env.COUNTER.idFromName("list-test-2");
await env.COUNTER.get(id1).increment();
await env.COUNTER.get(id2).increment();
// List all IDs in the namespace
const ids = await listDurableObjectIds(env.COUNTER);
expect(ids.length).toBeGreaterThanOrEqual(2);
expect(ids.some((id) => id.equals(id1))).toBe(true);
expect(ids.some((id) => id.equals(id2))).toBe(true);
});
});SQLite 支持的 Durable Objects 在测试中无缝工作。当 Durable Object 类在 Wrangler 配置中使用 new_sqlite_classes 配置时,SQL API 可用:
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("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);
});
});
});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);
});
});
});使用 runDurableObjectAlarm() 立即触发计划 alarm,无需等待计时器。这使您能够同步测试 alarm 处理程序:
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);
});
});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);
});
});要测试 alarm,请在 Durable Object 中添加 alarm() 方法:
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);
}
}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);
}
}使用 evictDurableObject() 在测试期间驱逐 Durable Object 实例。驱逐会拆除实例以重置其内存状态。这使您能够测试 Durable Object 在被驱逐后如何从存储恢复状态。
默认情况下,可休眠 WebSocket 会被休眠而非关闭,驱逐最多等待 30 秒让进行中的请求排空,然后才拆除实例。
以下测试同时设置内存状态(cachedHits)和 durable storage(计数器值),驱逐 Durable Object,并验证内存状态被清除而存储的计数得以保留:
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);
});
});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);
});
});您可以通过传递 options 参数控制 Durable Object 被驱逐时可休眠 WebSocket 的行为:
{ webSockets: "hibernate" }(默认)休眠 WebSocket,使其在驱逐后可以恢复。{ webSockets: "close" }在驱逐期间关闭 WebSocket。
以下示例使用接受 WebSocket 连接的 Durable Object,采用可休眠 WebSocket API:
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
}
}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
}
}在 Wrangler 配置中为 Durable Object 添加绑定(binding)和迁移,与现有 COUNTER 绑定一起:
{
"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" ]使用默认选项时,可休眠 WebSocket 在驱逐后保持打开,因此消息之后仍可往返。传递 { webSockets: "close" } 会改为关闭它们:
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();
});
});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();
});
});要一次驱逐所有当前运行的 Durable Objects(例如,在不删除持久化数据的情况下在测试之间重置状态),请使用 evictAllDurableObjects():
import { evictAllDurableObjects } from "cloudflare:test";
import { afterEach } from "vitest";
afterEach(async () => {
await evictAllDurableObjects();
});有关驱逐辅助函数的更多详情,包括 DurableObjectEvictionOptions 接口,请参阅测试 API 参考。
使用以下命令运行测试:
npx vitest或在 package.json 中添加脚本:
{
"scripts": {
"test": "vitest"
}
}- Workers Vitest 集成 - Vitest 集成的完整文档
- Durable Objects 测试示例 ↗ - Workers SDK 中的示例
- RPC 测试示例 ↗ - 使用 Durable Objects 测试 JSRPC