6.4 KiB
6.4 KiB
Testing Durable Objects
Use @cloudflare/vitest-pool-workers to test DOs inside the Workers runtime.
Setup
Install Dependencies
npm i -D vitest@~3.2.0 @cloudflare/vitest-pool-workers
vitest.config.ts
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.toml" },
},
},
},
});
TypeScript Config (test/tsconfig.json)
{
"extends": "../tsconfig.json",
"compilerOptions": {
"moduleResolution": "bundler",
"types": ["@cloudflare/vitest-pool-workers"]
},
"include": ["./**/*.ts", "../src/worker-configuration.d.ts"]
}
Environment Types (env.d.ts)
declare module "cloudflare:test" {
interface ProvidedEnv extends Env {}
}
Unit Tests (Direct DO Access)
import { env } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("Counter DO", () => {
it("should increment", async () => {
const stub = env.COUNTER.getByName("test-counter");
expect(await stub.increment()).toBe(1);
expect(await stub.increment()).toBe(2);
expect(await stub.getCount()).toBe(2);
});
it("isolates different instances", async () => {
const stub1 = env.COUNTER.getByName("counter-1");
const stub2 = env.COUNTER.getByName("counter-2");
await stub1.increment();
await stub1.increment();
await stub2.increment();
expect(await stub1.getCount()).toBe(2);
expect(await stub2.getCount()).toBe(1);
});
});
Integration Tests (HTTP via SELF)
import { SELF } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("Worker HTTP", () => {
it("should increment via POST", async () => {
const res = await SELF.fetch("http://example.com?id=test", {
method: "POST",
});
expect(res.status).toBe(200);
const data = await res.json<{ count: number }>();
expect(data.count).toBe(1);
});
it("should get count via GET", async () => {
await SELF.fetch("http://example.com?id=get-test", { method: "POST" });
await SELF.fetch("http://example.com?id=get-test", { method: "POST" });
const res = await SELF.fetch("http://example.com?id=get-test");
const data = await res.json<{ count: number }>();
expect(data.count).toBe(2);
});
});
Direct Internal Access
Use runInDurableObject() to access instance internals and storage:
import { env, runInDurableObject } from "cloudflare:test";
import { describe, it, expect } from "vitest";
import { Counter } from "../src";
describe("DO internals", () => {
it("can verify storage directly", async () => {
const stub = env.COUNTER.getByName("direct-test");
await stub.increment();
await stub.increment();
await runInDurableObject(stub, async (instance: Counter, state) => {
expect(instance).toBeInstanceOf(Counter);
const result = state.storage.sql
.exec<{ value: number }>(
"SELECT value FROM counters WHERE name = ?",
"default"
)
.one();
expect(result.value).toBe(2);
});
});
});
List DO IDs
import { env, listDurableObjectIds } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("DO listing", () => {
it("can list all IDs in namespace", async () => {
const id1 = env.COUNTER.idFromName("list-1");
const id2 = env.COUNTER.idFromName("list-2");
await env.COUNTER.get(id1).increment();
await env.COUNTER.get(id2).increment();
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);
});
});
Testing Alarms
Use runDurableObjectAlarm() to trigger alarms immediately:
import { env, runInDurableObject, runDurableObjectAlarm } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("DO alarms", () => {
it("can trigger alarms immediately", async () => {
const stub = env.COUNTER.getByName("alarm-test");
await stub.increment();
await stub.increment();
expect(await stub.getCount()).toBe(2);
// Schedule alarm
await runInDurableObject(stub, async (instance, state) => {
await state.storage.setAlarm(Date.now() + 60_000);
});
// Execute immediately without waiting
const ran = await runDurableObjectAlarm(stub);
expect(ran).toBe(true);
// Verify alarm handler ran (if it resets counter)
expect(await stub.getCount()).toBe(0);
// No alarm scheduled now
const ranAgain = await runDurableObjectAlarm(stub);
expect(ranAgain).toBe(false);
});
});
Example alarm handler:
async alarm(): Promise<void> {
this.ctx.storage.sql.exec("DELETE FROM counters");
}
Test Isolation
Each test gets isolated storage automatically. DOs from one test don't affect others:
describe("Isolation", () => {
it("first test creates DO", async () => {
const stub = env.COUNTER.getByName("isolated");
await stub.increment();
expect(await stub.getCount()).toBe(1);
});
it("second test has fresh state", async () => {
const ids = await listDurableObjectIds(env.COUNTER);
expect(ids.length).toBe(0); // Previous test's DO is gone
const stub = env.COUNTER.getByName("isolated");
expect(await stub.getCount()).toBe(0); // Fresh instance
});
});
SQLite Storage Testing
describe("SQLite", () => {
it("can verify SQL storage", async () => {
const stub = env.COUNTER.getByName("sqlite-test");
await stub.increment("page-views");
await stub.increment("page-views");
await stub.increment("api-calls");
await runInDurableObject(stub, async (instance, state) => {
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 },
]);
expect(state.storage.sql.databaseSize).toBeGreaterThan(0);
});
});
});
Running Tests
npx vitest # Watch mode
npx vitest run # Single run
package.json:
{
"scripts": {
"test": "vitest"
}
}