# Testing Durable Objects

Use `@cloudflare/vitest-pool-workers` to test DOs inside the Workers runtime.

## Setup

### Install Dependencies

```bash
npm i -D vitest@~3.2.0 @cloudflare/vitest-pool-workers
```

### vitest.config.ts

```typescript
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'

export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        wrangler: { configPath: './wrangler.toml' },
      },
    },
  },
})
```

### TypeScript Config (test/tsconfig.json)

```jsonc
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "moduleResolution": "bundler",
    "types": ["@cloudflare/vitest-pool-workers"],
  },
  "include": ["./**/*.ts", "../src/worker-configuration.d.ts"],
}
```

### Environment Types (env.d.ts)

```typescript
declare module 'cloudflare:test' {
  interface ProvidedEnv extends Env {}
}
```

## Unit Tests (Direct DO Access)

```typescript
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)

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

```typescript
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

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

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

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

```typescript
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

```typescript
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

```bash
npx vitest        # Watch mode
npx vitest run    # Single run
```

package.json:

```json
{
  "scripts": {
    "test": "vitest"
  }
}
```
