---
title: Testing Durable Objects
description: Write tests for Durable Objects using the Workers Vitest integration.
image: https://developers.cloudflare.com/dev-products-preview.png
---

> 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.

[Skip to content](#%5Ftop) 

# Testing Durable Objects

**Last reviewed:**  almost 3 years ago 

Write tests for Durable Objects using the Workers Vitest integration.

Use the [@cloudflare/vitest-pool-workers ↗](https://www.npmjs.com/package/@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:

* [ npm ](#tab-panel-6060)
* [ pnpm ](#tab-panel-6061)
* [ yarn ](#tab-panel-6062)

Terminal window

```

npm i -D vitest@^4.1.0 @cloudflare/vitest-pool-workers


```

Terminal window

```

pnpm add -D vitest@^4.1.0 @cloudflare/vitest-pool-workers


```

Terminal window

```

yarn add -D vitest@^4.1.0 @cloudflare/vitest-pool-workers


```

## Example Durable Object

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

* [  JavaScript ](#tab-panel-6075)
* [  TypeScript ](#tab-panel-6076)

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 });

  },

};


```

src/index.ts

```

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

        )

      `);

    });

  }


  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.ts

```

import { cloudflareTest } from "@cloudflare/vitest-pool-workers";

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:

* [  wrangler.jsonc ](#tab-panel-6063)
* [  wrangler.toml ](#tab-panel-6064)

JSONC

```

{

  "name": "counter-worker",

  "main": "src/index.ts",

  // Set this to today's date

  "compatibility_date": "2026-05-08",

  "durable_objects": {

    "bindings": [

      { "name": "COUNTER", "class_name": "Counter" }

    ]

  },

  "migrations": [

    { "tag": "v1", "new_sqlite_classes": ["Counter"] }

  ]

}


```

TOML

```

name = "counter-worker"

main = "src/index.ts"

# Set this to today's date

compatibility_date = "2026-05-08"


[[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: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`:

* [  JavaScript ](#tab-panel-6073)
* [  TypeScript ](#tab-panel-6074)

test/counter.test.js

```

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.ts

```

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:

* [  JavaScript ](#tab-panel-6077)
* [  TypeScript ](#tab-panel-6078)

test/integration.test.js

```

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.ts

```

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:

* [  JavaScript ](#tab-panel-6071)
* [  TypeScript ](#tab-panel-6072)

test/direct-access.test.js

```

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).toBe(2);

    expect(ids.some((id) => id.equals(id1))).toBe(true);

    expect(ids.some((id) => id.equals(id2))).toBe(true);

  });

});


```

test/direct-access.test.ts

```

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).toBe(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:

* [  JavaScript ](#tab-panel-6067)
* [  TypeScript ](#tab-panel-6068)

test/sqlite.test.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 },

      ]);


      // Check database size is non-zero

      expect(state.storage.sql.databaseSize).toBeGreaterThan(0);

    });

  });

});


```

test/sqlite.test.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:

* [  JavaScript ](#tab-panel-6069)
* [  TypeScript ](#tab-panel-6070)

test/alarm.test.js

```

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.ts

```

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:

* [  JavaScript ](#tab-panel-6065)
* [  TypeScript ](#tab-panel-6066)

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);

  }

}


```

src/index.ts

```

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);

  }

}


```

## Running tests

Run your tests with:

Terminal window

```

npx vitest


```

Or add a script to your `package.json`:

```

{

  "scripts": {

    "test": "vitest"

  }

}


```

## Related resources

* [Workers Vitest integration](https://developers.cloudflare.com/workers/testing/vitest-integration/) \- Full documentation for the Vitest integration
* [Durable Objects testing recipe ↗](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples/durable-objects) \- Example from the Workers SDK
* [RPC testing recipe ↗](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples/rpc) \- Testing JSRPC with Durable Objects

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/durable-objects/","name":"Durable Objects"}},{"@type":"ListItem","position":3,"item":{"@id":"/durable-objects/examples/","name":"Examples"}},{"@type":"ListItem","position":4,"item":{"@id":"/durable-objects/examples/testing-with-durable-objects/","name":"Testing Durable Objects"}}]}
```
