Testing Durable Objects
Write tests for Durable Objects using the Workers Vitest integration.
Last updated Oct 12, 2026
Use the @cloudflare/vitest-plugin ↗︎ package to write tests for your Durable Objects. This integration runs your tests inside the Workers runtime, giving you direct access to Durable Object bindings and APIs.
Prerequisites
Install Vitest and the Workers Vitest integration as dev dependencies:
npm i -D vitest@^4.1.0 @cloudflare/vitest-plugin
pnpm add -D vitest@^4.1.0 @cloudflare/vitest-plugin
yarn add -D vitest@^4.1.0 @cloudflare/vitest-plugin
Example Durable Object
This example tests a simple counter Durable Object with SQLite storage:
src/index.jsjs
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 });
},
};
src/index.tsts
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 });
},
};
Configure Vitest
Create a vitest.config.ts file that uses the cloudflareTest() plugin:
vitest.config.tsts
import { cloudflareTest } from "@cloudflare/vitest-plugin";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [
cloudflareTest({
wrangler: { configPath: "./wrangler.jsonc" },
}),
],
});
Make sure your Wrangler configuration includes the Durable Object binding and SQLite migration:
{
"name": "counter-worker",
"main": "src/index.ts",
"upload_source_maps": true,
// Set this to today's date
"compatibility_date": "2026-10-27",
"durable_objects": {
"bindings": [
{ "name": "COUNTER", "class_name": "Counter" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Counter"] }
]
}
name = "counter-worker"
main = "src/index.ts"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-10-27"
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "Counter" ]
Define types for tests
Create a test/tsconfig.json to configure TypeScript for your tests:
test/tsconfig.jsonjsonc
{
"extends": "../tsconfig.json",
"compilerOptions": {
"moduleResolution": "bundler",
"types": ["@cloudflare/vitest-plugin/types"]
},
"include": ["./**/*.ts", "../src/worker-configuration.d.ts"]
}
Create an env.d.ts file to type the test environment:
test/env.d.tsts
declare module "cloudflare:workers" {
interface ProvidedEnv extends Env {}
}
Writing tests
Unit tests with direct Durable Object access
You can get a stub to a Durable Object directly from the env object provided by cloudflare:workers:
test/counter.test.jsjs
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);
});
});
test/counter.test.tsts
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);
});
});
Integration tests with exports
Use exports.default.fetch() to test your Worker's HTTP handler, which routes requests to Durable Objects:
test/integration.test.jsjs
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);
});
});
test/integration.test.tsts
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);
});
});
Direct access to Durable Object internals
Use runInDurableObject() to access instance properties and storage directly. This is useful for verifying internal state or testing private methods:
test/direct-access.test.jsjs
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);
});
});
test/direct-access.test.tsts
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);
});
});
Testing SQLite storage
SQLite-backed Durable Objects work seamlessly in tests. The SQL API is available when your Durable Object class is configured with new_sqlite_classes in your Wrangler configuration:
test/sqlite.test.jsjs
```js 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 }, ]);
