agentsclimarketplace

Cloudflare workers testing

Skill secondsky/claude-skills/plugins/cloudflare-workers/skills/cloudflare-workers-testing

Production-ready skills for Claude Code CLI - Cloudflare, React, Tailwind v4, and AI integrations

Install
npx -y skills add secondsky/claude-skills --skill cloudflare-workers-testing

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

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.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

20.6 KB, ~5.2k tokens by cl100k_base, as published. Nobody here has run it

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?

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:

# 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

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

2. Create vitest.config.ts

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

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

bun test
# or
bunx vitest

Critical Rules

1. Always Use cloudflare:test for Env Access

✅ CORRECT:

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:

// 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:

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:

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:

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:

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

❌ WRONG:

// 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:

// 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:

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:

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:

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:

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

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

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:

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:

bunx vitest run --coverage

Top 5 Use Cases

1. Testing API Endpoints with D1

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

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

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

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

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:

    it('returns 404 when user not found', async () => {});
    it('validates email format before saving', async () => {});
    
  2. Test error cases:

    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:

    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:

    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:

    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:

    // ❌ 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:

    // ❌ 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:

    // ❌ 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:

    // ❌ 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:

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:

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:

// 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:

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:

// ❌ 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:

// ❌ 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:

// 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:

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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.