Skip to content
Cloudflare Docs

Testing Durable Objects

Write tests for Durable Objects using the Workers Vitest integration.

Use the @cloudflare/vitest-pool-workers 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:

Terminal window
npm i -D vitest@~3.2.0 @cloudflare/vitest-pool-workers

Example Durable Object

This example tests a simple counter Durable Object with SQLite storage:

src/index.js
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
)
`);
});
}
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 });
},
};

Configure Vitest

Create a vitest.config.ts file that uses defineWorkersConfig:

vitest.config.ts
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.toml" },
},
},
},
});

Make sure your Wrangler configuration includes the Durable Object binding and SQLite migration:

{
"name": "counter-worker",
"main": "src/index.ts",
"compatibility_date": "2024-12-01",
"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.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"moduleResolution": "bundler",
"types": ["@cloudflare/vitest-pool-workers"]
},
"include": ["./**/*.ts", "../src/worker-configuration.d.ts"]
}

Create an env.d.ts file to type the test environment:

test/env.d.ts
declare module "cloudflare:test" {
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:test:

test/counter.test.js
import { env } from "cloudflare:test";
import { describe, it, expect, beforeEach } from "vitest";
describe("Counter Durable Object", () => {
// Each test gets isolated storage automatically
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 track separate counters independently", async () => {
const id = env.COUNTER.idFromName("test-counter");
const stub = env.COUNTER.get(id);
await stub.increment("counter-a");
await stub.increment("counter-a");
await stub.increment("counter-b");
expect(await stub.getCount("counter-a")).toBe(2);
expect(await stub.getCount("counter-b")).toBe(1);
expect(await stub.getCount("counter-c")).toBe(0);
});
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 SELF

Use the SELF fetcher to test your Worker's HTTP handler, which routes requests to Durable Objects:

test/integration.test.js
import { SELF } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("Counter Worker integration", () => {
it("should increment via HTTP POST", async () => {
const response = await SELF.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 SELF.fetch("http://example.com?id=get-test", { method: "POST" });
await SELF.fetch("http://example.com?id=get-test", { method: "POST" });
// Then get the count
const response = await SELF.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 SELF.fetch("http://example.com?id=counter-a", { method: "POST" });
await SELF.fetch("http://example.com?id=counter-a", { method: "POST" });
await SELF.fetch("http://example.com?id=counter-b", { method: "POST" });
const responseA = await SELF.fetch("http://example.com?id=counter-a");
const responseB = await SELF.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);
});
});

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.js
import { env, 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).toBe(2);
expect(ids.some((id) => id.equals(id1))).toBe(true);
expect(ids.some((id) => id.equals(id2))).toBe(true);
});
});

Test isolation

Each test automatically gets isolated storage. Durable Objects created in one test do not affect other tests:

test/isolation.test.js
import { env, listDurableObjectIds } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("Test isolation", () => {
it("first test: creates a Durable Object", async () => {
const id = env.COUNTER.idFromName("isolated-counter");
const stub = env.COUNTER.get(id);
await stub.increment();
await stub.increment();
expect(await stub.getCount()).toBe(2);
});
it("second test: previous Durable Object does not exist", async () => {
// The Durable Object from the previous test is automatically cleaned up
const ids = await listDurableObjectIds(env.COUNTER);
expect(ids.length).toBe(0);
// Creating the same ID gives a fresh instance
const id = env.COUNTER.idFromName("isolated-counter");
const stub = env.COUNTER.get(id);
expect(await stub.getCount()).toBe(0);
});
});

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.js
import { env, 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);
});
});
});

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.js
import {
env,
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.js
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);
}
}

Running tests

Run your tests with:

Terminal window
npx vitest

Or add a script to your package.json:

{
"scripts": {
"test": "vitest"
}
}