Back to skills
SkillHub ClubShip Full StackFull StackTesting

cloudflare-workers-testing

Imported from https://github.com/secondsky/claude-skills.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
84
Hot score
93
Updated
March 20, 2026
Overall rating
C4.7
Composite score
4.7
Best-practice grade
F19.6

Install command

npx @skill-hub/cli install secondsky-claude-skills-cloudflare-workers-testing

Repository

secondsky/claude-skills

Skill path: plugins/cloudflare-workers/skills/cloudflare-workers-testing

Imported from https://github.com/secondsky/claude-skills.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Testing.

Target audience: everyone.

License: MIT.

Original source

Catalog source: SkillHub Club.

Repository owner: secondsky.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install cloudflare-workers-testing into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/secondsky/claude-skills before adding cloudflare-workers-testing to shared team environments
  • Use cloudflare-workers-testing for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: workers-testing
description: Comprehensive testing guide for Cloudflare Workers using Vitest and @cloudflare/vitest-pool-workers. Use for test setup, binding mocks (D1/KV/R2/DO), integration tests, or encountering test failures, mock errors, coverage issues.
keywords:
  - cloudflare-workers
  - workers-testing
  - vitest
  - vitest-workers
  - miniflare
  - cloudflare-test
  - unit-testing
  - integration-testing
  - binding-mocks
  - d1-testing
  - kv-testing
  - r2-testing
  - durable-objects-testing
  - queue-testing
  - workers-ai-testing
  - test-coverage
  - test-failures
  - mock-errors
  - @cloudflare/vitest-pool-workers
  - cloudflare:test
  - env-mocking
  - execution-context
  - workers-test-setup
  - vitest-config
  - test-driven-development
  - tdd-workers
license: MIT
metadata:
  version: "1.0.0"
  last_verified: "2025-01-27"
  production_tested: true
  token_savings: "~70%"
  errors_prevented: 8
  templates_included: 3
  references_included: 5
  scripts_included: 2
  vitest_version: "2.1.8"
  workers_types_version: "4.20251125.0"
  vitest_pool_workers_version: "0.7.2"
---

# Cloudflare Workers Testing with Vitest

**Status**: ✅ Production Ready | Last Verified: 2025-01-27
**Vitest**: 2.1.8 | **@cloudflare/vitest-pool-workers**: 0.7.2 | **Miniflare**: Latest

## Table of Contents

- [What Is Workers Testing?](#what-is-workers-testing)
- [New in 2025](#new-in-2025)
- [Quick Start (5 Minutes)](#quick-start-5-minutes)
- [Critical Rules](#critical-rules)
- [Core Concepts](#core-concepts)
- [Top 5 Use Cases](#top-5-use-cases)
- [Best Practices](#best-practices)
- [Top 8 Errors Prevented](#top-8-errors-prevented)
- [When to Load References](#when-to-load-references)

---

## What Is Workers Testing?

Testing Cloudflare Workers with **Vitest** and **@cloudflare/vitest-pool-workers** enables writing unit and integration tests that run in a real Workers environment with full binding support (D1, KV, R2, Durable Objects, Queues, AI). Tests execute in Miniflare for local development and can run in CI/CD with actual Workers runtime behavior.

**Key capabilities**: Binding mocks, execution context testing, edge runtime simulation, coverage tracking, fast test execution.

---

## New in 2025

**@cloudflare/vitest-pool-workers 0.7.2** (January 2025):
- **BREAKING**: Miniflare v3 → requires Node.js 18+
- **NEW**: `cloudflare:test` module for env/ctx access
- **IMPROVED**: Faster isolated storage for bindings
- **FIXED**: Worker-to-worker service bindings now work correctly
- **ADDED**: Support for Vectorize and Workers AI bindings

**Migration from older versions**:
```bash
# Update dependencies
bun add -D vitest@^2.1.8 @cloudflare/vitest-pool-workers@^0.7.2

# Update vitest.config.ts (new pool configuration format)
export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        wrangler: { configPath: './wrangler.jsonc' },
        miniflare: { compatibilityDate: '2025-01-27' }
      }
    }
  }
});
```

---

## Quick Start (5 Minutes)

### 1. Install Dependencies

```bash
bun add -D vitest @cloudflare/vitest-pool-workers
```

### 2. Create `vitest.config.ts`

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

export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        wrangler: { configPath: './wrangler.jsonc' },
        miniflare: {
          compatibilityDate: '2025-01-27',
          compatibilityFlags: ['nodejs_compat']
        }
      }
    }
  }
});
```

### 3. Write Your First Test

```typescript
import { describe, it, expect } from 'vitest';
import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test';
import worker from '../src/index';

describe('Worker', () => {
  it('responds with 200', async () => {
    const request = new Request('http://example.com/');
    const ctx = createExecutionContext();
    const response = await worker.fetch(request, env, ctx);
    await waitOnExecutionContext(ctx);

    expect(response.status).toBe(200);
  });
});
```

### 4. Run Tests

```bash
bun test
# or
bunx vitest
```

---

## Critical Rules

### 1. Always Use `cloudflare:test` for Env Access

**✅ CORRECT**:
```typescript
import { env } from 'cloudflare:test';

it('queries D1', async () => {
  const result = await env.DB.prepare('SELECT * FROM users').all();
  expect(result.results).toHaveLength(0); // Fresh isolated DB per test
});
```

**❌ WRONG**:
```typescript
// Don't manually create env object
const env = { DB: mockDB }; // ❌ Won't use real D1 binding
```

**Why**: `cloudflare:test` provides real bindings configured from `wrangler.jsonc` with isolated storage per test.

### 2. Always Wait on Execution Context

**✅ CORRECT**:
```typescript
it('handles async operations', async () => {
  const ctx = createExecutionContext();
  const response = await worker.fetch(request, env, ctx);
  await waitOnExecutionContext(ctx); // ✅ Ensures ctx.waitUntil completes

  expect(response.status).toBe(200);
});
```

**❌ WRONG**:
```typescript
it('missing wait', async () => {
  const ctx = createExecutionContext();
  const response = await worker.fetch(request, env, ctx);
  // ❌ Missing waitOnExecutionContext - ctx.waitUntil tasks may not complete
  expect(response.status).toBe(200);
});
```

**Why**: Workers use `ctx.waitUntil()` for background tasks (logging, analytics). Without waiting, these tasks may not complete in tests.

### 3. Each Test Gets Isolated Storage

**✅ CORRECT**:
```typescript
describe('KV Operations', () => {
  it('test 1: writes to KV', async () => {
    await env.CACHE.put('key', 'value1');
    const val = await env.CACHE.get('key');
    expect(val).toBe('value1'); // ✅ Isolated
  });

  it('test 2: clean state', async () => {
    const val = await env.CACHE.get('key');
    expect(val).toBeNull(); // ✅ Test 1's data doesn't leak here
  });
});
```

**Why**: Each test runs with fresh binding storage (automatic isolation).

### 4. Use Wrangler Config for Bindings

**✅ CORRECT**:
```typescript
// vitest.config.ts
export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        wrangler: { configPath: './wrangler.jsonc' } // ✅ Reads bindings from wrangler
      }
    }
  }
});
```

**❌ WRONG**:
```typescript
// vitest.config.ts
export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        // ❌ No wrangler config - bindings won't be available
        miniflare: { compatibilityDate: '2025-01-27' }
      }
    }
  }
});
```

**Why**: Wrangler config defines all bindings (D1, KV, R2, etc.). Without it, `env` will be empty.

### 5. Match Compatibility Date

**✅ CORRECT**:
```typescript
// vitest.config.ts
miniflare: {
  compatibilityDate: '2025-01-27' // ✅ Matches wrangler.jsonc
}

// wrangler.jsonc
{
  "compatibility_date": "2025-01-27"
}
```

**Why**: Ensures test environment matches production runtime behavior.

---

## Core Concepts

### Binding Testing Patterns

**D1 Database**:
```typescript
import { env } from 'cloudflare:test';

it('queries D1', async () => {
  // Insert test data
  await env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice').run();

  // Query
  const result = await env.DB.prepare('SELECT * FROM users WHERE name = ?').bind('Alice').first();
  expect(result?.name).toBe('Alice');
});
```

**KV Namespace**:
```typescript
it('reads from KV', async () => {
  await env.CACHE.put('test-key', 'test-value');
  const value = await env.CACHE.get('test-key');
  expect(value).toBe('test-value');
});
```

**R2 Bucket**:
```typescript
it('uploads to R2', async () => {
  await env.BUCKET.put('file.txt', 'Hello World');
  const object = await env.BUCKET.get('file.txt');
  expect(await object?.text()).toBe('Hello World');
});
```

**Durable Objects**:
```typescript
it('interacts with Durable Object', async () => {
  const id = env.COUNTER.idFromName('test-counter');
  const stub = env.COUNTER.get(id);

  const response = await stub.fetch('http://fake/increment');
  const data = await response.json();
  expect(data.count).toBe(1);
});
```

### Unit vs Integration Tests

**Unit Test** (single function):
```typescript
import { validateInput } from '../src/utils/validator';

it('validates input', () => {
  const result = validateInput({ name: 'Alice', age: 30 });
  expect(result.valid).toBe(true);
});
```

**Integration Test** (full fetch handler):
```typescript
import worker from '../src/index';
import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test';

it('handles full request flow', async () => {
  const request = new Request('http://example.com/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Alice' })
  });

  const ctx = createExecutionContext();
  const response = await worker.fetch(request, env, ctx);
  await waitOnExecutionContext(ctx);

  expect(response.status).toBe(201);
  const user = await response.json();
  expect(user.name).toBe('Alice');
});
```

### Coverage Configuration

Add to `vitest.config.ts`:
```typescript
export default defineWorkersConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      include: ['src/**/*.ts'],
      exclude: ['src/**/*.test.ts', 'src/**/*.spec.ts'],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 80,
        statements: 80
      }
    }
  }
});
```

Run with coverage:
```bash
bunx vitest run --coverage
```

---

## Top 5 Use Cases

### 1. Testing API Endpoints with D1

```typescript
it('creates user via API', async () => {
  const request = new Request('http://example.com/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Bob', email: '[email protected]' })
  });

  const ctx = createExecutionContext();
  const response = await worker.fetch(request, env, ctx);
  await waitOnExecutionContext(ctx);

  expect(response.status).toBe(201);

  // Verify DB insert
  const user = await env.DB.prepare('SELECT * FROM users WHERE email = ?')
    .bind('[email protected]')
    .first();
  expect(user?.name).toBe('Bob');
});
```

### 2. Testing Caching with KV

```typescript
it('caches API responses', async () => {
  // First request (cache miss)
  const req1 = new Request('http://example.com/api/data');
  const ctx1 = createExecutionContext();
  const res1 = await worker.fetch(req1, env, ctx1);
  await waitOnExecutionContext(ctx1);

  expect(res1.headers.get('X-Cache')).toBe('MISS');

  // Second request (cache hit)
  const req2 = new Request('http://example.com/api/data');
  const ctx2 = createExecutionContext();
  const res2 = await worker.fetch(req2, env, ctx2);
  await waitOnExecutionContext(ctx2);

  expect(res2.headers.get('X-Cache')).toBe('HIT');
});
```

### 3. Testing File Uploads to R2

```typescript
it('handles file upload', async () => {
  const formData = new FormData();
  formData.append('file', new Blob(['test content'], { type: 'text/plain' }), 'test.txt');

  const request = new Request('http://example.com/upload', {
    method: 'POST',
    body: formData
  });

  const ctx = createExecutionContext();
  const response = await worker.fetch(request, env, ctx);
  await waitOnExecutionContext(ctx);

  expect(response.status).toBe(200);

  // Verify R2 upload
  const object = await env.BUCKET.get('test.txt');
  expect(await object?.text()).toBe('test content');
});
```

### 4. Testing Durable Objects State

```typescript
it('maintains counter state', async () => {
  const id = env.COUNTER.idFromName('my-counter');
  const stub = env.COUNTER.get(id);

  // Increment 3 times
  for (let i = 0; i < 3; i++) {
    await stub.fetch('http://fake/increment');
  }

  // Verify state
  const response = await stub.fetch('http://fake/value');
  const data = await response.json();
  expect(data.count).toBe(3);
});
```

### 5. Testing Queue Consumers

```typescript
it('processes queue messages', async () => {
  const messages = [
    { id: '1', body: { action: 'email', to: '[email protected]' }, timestamp: new Date() }
  ];

  // Simulate queue batch
  await worker.queue(
    {
      queue: 'my-queue',
      messages,
      retryAll: () => {},
      ackAll: () => {}
    },
    env
  );

  // Verify processing (check DB, logs, etc.)
  const log = await env.DB.prepare('SELECT * FROM email_log WHERE id = ?').bind('1').first();
  expect(log?.status).toBe('sent');
});
```

---

## Best Practices

### ✅ DO

1. **Use descriptive test names**:
   ```typescript
   it('returns 404 when user not found', async () => {});
   it('validates email format before saving', async () => {});
   ```

2. **Test error cases**:
   ```typescript
   it('returns 400 for invalid JSON', async () => {
     const request = new Request('http://example.com/api', {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: 'invalid json'
     });

     const ctx = createExecutionContext();
     const response = await worker.fetch(request, env, ctx);
     await waitOnExecutionContext(ctx);

     expect(response.status).toBe(400);
   });
   ```

3. **Group related tests**:
   ```typescript
   describe('User API', () => {
     describe('POST /users', () => {
       it('creates user with valid data', async () => {});
       it('rejects duplicate email', async () => {});
       it('validates required fields', async () => {});
     });
   });
   ```

4. **Use beforeEach for setup**:
   ```typescript
   describe('Database tests', () => {
     beforeEach(async () => {
       // Seed test data
       await env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Test User').run();
     });

     it('queries users', async () => {
       const result = await env.DB.prepare('SELECT * FROM users').all();
       expect(result.results).toHaveLength(1);
     });
   });
   ```

5. **Test realistic scenarios**:
   ```typescript
   it('handles concurrent requests', async () => {
     const requests = Array(10).fill(null).map(() =>
       worker.fetch(new Request('http://example.com/'), env, createExecutionContext())
     );

     const responses = await Promise.all(requests);
     expect(responses.every(r => r.status === 200)).toBe(true);
   });
   ```

### ❌ DON'T

1. **Don't share state between tests**:
   ```typescript
   // ❌ BAD: Leaky state
   let counter = 0;
   it('test 1', () => { counter++; });
   it('test 2', () => { expect(counter).toBe(1); }); // Fragile!

   // ✅ GOOD: Isolated
   it('test 1', () => { const counter = 0; counter++; });
   it('test 2', () => { const counter = 0; /* fresh state */ });
   ```

2. **Don't forget to wait**:
   ```typescript
   // ❌ BAD
   const response = await worker.fetch(request, env, ctx);
   expect(response.status).toBe(200); // ctx.waitUntil not finished

   // ✅ GOOD
   const response = await worker.fetch(request, env, ctx);
   await waitOnExecutionContext(ctx);
   expect(response.status).toBe(200);
   ```

3. **Don't hardcode URLs**:
   ```typescript
   // ❌ BAD
   const request = new Request('http://example.com/test');

   // ✅ GOOD
   const request = new Request('http://fake-host/test'); // Host doesn't matter in tests
   ```

4. **Don't test implementation details**:
   ```typescript
   // ❌ BAD: Testing internals
   expect(worker.privateHelperFunction).toBeDefined();

   // ✅ GOOD: Testing behavior
   const response = await worker.fetch(request, env, ctx);
   expect(response.status).toBe(200);
   ```

---

## Top 8 Errors Prevented

### 1. ❌ `ReferenceError: env is not defined`

**Cause**: Not importing `env` from `cloudflare:test`.

**Fix**:
```typescript
import { env } from 'cloudflare:test'; // ✅ Add this import
```

**Prevention**: Always use `cloudflare:test` module for env access.

---

### 2. ❌ `TypeError: Cannot read property 'DB' of undefined`

**Cause**: `wrangler.jsonc` not loaded in `vitest.config.ts`.

**Fix**:
```typescript
export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        wrangler: { configPath: './wrangler.jsonc' } // ✅ Add this
      }
    }
  }
});
```

**Prevention**: Always configure wrangler path in vitest config.

---

### 3. ❌ `Error: D1_ERROR: no such table: users`

**Cause**: D1 database schema not applied in tests.

**Fix**:
```typescript
// Option 1: Seed in beforeEach
beforeEach(async () => {
  await env.DB.exec(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY,
      name TEXT NOT NULL
    )
  `);
});

// Option 2: Use migrations (load from file)
beforeEach(async () => {
  const schema = await fs.readFile('./migrations/schema.sql', 'utf-8');
  await env.DB.exec(schema);
});
```

**Prevention**: Create schema before each test or use shared setup.

---

### 4. ❌ `Error: ctx.waitUntil tasks did not complete`

**Cause**: Missing `await waitOnExecutionContext(ctx)`.

**Fix**:
```typescript
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx); // ✅ Add this
```

**Prevention**: Always wait on execution context in tests.

---

### 5. ❌ `Error: SELF is not defined`

**Cause**: Using old `SELF.fetch()` pattern instead of direct worker import.

**Fix**:
```typescript
// ❌ OLD (vitest-pool-workers <0.5)
import { SELF } from 'cloudflare:test';
await SELF.fetch(request);

// ✅ NEW (vitest-pool-workers ≥0.7)
import worker from '../src/index';
await worker.fetch(request, env, ctx);
```

**Prevention**: Use direct worker imports (modern pattern).

---

### 6. ❌ `Error: KV.get() returned data from previous test`

**Cause**: Believing storage is shared (it's not, but may indicate test leak).

**Fix**: Each test is isolated. If seeing this, check for:
```typescript
// ❌ Test pollution (shared variable)
let cache = {};
it('test 1', () => { cache.key = 'value'; });
it('test 2', () => { expect(cache.key).toBeUndefined(); }); // Fails!

// ✅ Proper isolation
it('test 1', async () => { await env.CACHE.put('key', 'value1'); });
it('test 2', async () => { const val = await env.CACHE.get('key'); expect(val).toBeNull(); });
```

**Prevention**: Don't use shared variables for test data.

---

### 7. ❌ `TypeError: env.BUCKET.put is not a function`

**Cause**: R2 binding not configured in `wrangler.jsonc`.

**Fix**:
```jsonc
// wrangler.jsonc
{
  "r2_buckets": [
    { "binding": "BUCKET", "bucket_name": "test-bucket" }
  ]
}
```

**Prevention**: Define all bindings in wrangler config.

---

### 8. ❌ `Error: Pool 'workers' is not supported`

**Cause**: Missing `@cloudflare/vitest-pool-workers` dependency.

**Fix**:
```bash
bun add -D @cloudflare/vitest-pool-workers
```

**Prevention**: Install pool package for Workers testing.

---

## When to Load References

Load reference files for detailed, specialized content:

**Load `references/vitest-setup.md` when:**
- Setting up Vitest from scratch
- Configuring custom pool options
- Troubleshooting Miniflare configuration
- Migrating from older vitest-pool-workers versions

**Load `references/binding-mocks.md` when:**
- Testing specific bindings (D1, KV, R2, DO, Queues, AI, Vectorize)
- Mocking service bindings (worker-to-worker)
- Creating test fixtures for bindings
- Understanding isolated storage behavior

**Load `references/integration-testing.md` when:**
- Writing full request/response tests
- Testing multi-step workflows
- Simulating production scenarios
- Testing WebSocket or streaming responses

**Load `references/coverage-optimization.md` when:**
- Setting up coverage thresholds
- Identifying untested code paths
- Optimizing test suite performance
- Configuring coverage reporters

**Load `references/troubleshooting.md` when:**
- Debugging failing tests
- Resolving binding errors
- Fixing timeout issues
- Understanding error messages

**Load `templates/vitest-config.ts` for:**
- Complete vitest.config.ts example
- Advanced configuration options
- Multiple wrangler environments

**Load `templates/basic-test.ts` for:**
- Test file structure template
- Common test patterns
- beforeEach/afterEach examples

**Load `templates/binding-mock-test.ts` for:**
- Binding-specific test examples
- D1, KV, R2, DO test patterns
- Queue and AI testing examples

**Load `scripts/setup-vitest.sh` for:**
- Automated Vitest installation
- Project configuration script

**Load `scripts/run-tests.sh` for:**
- CI/CD test execution
- Coverage reporting automation

---

## Related Cloudflare Plugins

**For service-specific testing patterns, load:**
- **cloudflare-d1** - D1 database testing, migrations, seeding
- **cloudflare-kv** - KV namespace testing, TTL verification
- **cloudflare-r2** - R2 bucket testing, file upload/download
- **cloudflare-durable-objects** - DO testing, WebSocket testing
- **cloudflare-queues** - Queue testing, batch processing
- **cloudflare-workers-ai** - AI model testing, inference mocking

**This skill focuses on cross-cutting Workers testing patterns** applicable to ALL binding types and Workers features.

---

**Questions?** Load `references/troubleshooting.md` or use `/workers-debug` command for interactive help.
cloudflare-workers-testing | SkillHub