Claude-skill-registry workers-testing
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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/cloudflare-workers-testing" ~/.claude/skills/majiayu000-claude-skill-registry-workers-testing && rm -rf "$T"
skills/data/cloudflare-workers-testing/SKILL.mdCloudflare 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?
- New in 2025
- Quick Start (5 Minutes)
- Critical Rules
- Core Concepts
- Top 5 Use Cases
- Best Practices
- Top 8 Errors Prevented
- 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:
module for env/ctx accesscloudflare:test - 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
vitest.config.tsimport { 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
cloudflare:test✅ 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: 'bob@example.com' }) }); 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('bob@example.com') .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: 'user@example.com' }, 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
-
Use descriptive test names:
it('returns 404 when user not found', async () => {}); it('validates email format before saving', async () => {}); -
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); }); -
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 () => {}); }); }); -
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); }); }); -
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
-
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 */ }); -
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); -
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 -
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
ReferenceError: env is not definedCause: 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
TypeError: Cannot read property 'DB' of undefinedCause:
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
Error: D1_ERROR: no such table: usersCause: 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
Error: ctx.waitUntil tasks did not completeCause: 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
Error: SELF is not definedCause: 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
Error: KV.get() returned data from previous testCause: 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
TypeError: env.BUCKET.put is not a functionCause: 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
Error: Pool 'workers' is not supportedCause: 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
when:references/vitest-setup.md
- Setting up Vitest from scratch
- Configuring custom pool options
- Troubleshooting Miniflare configuration
- Migrating from older vitest-pool-workers versions
Load
when:references/binding-mocks.md
- 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
when:references/integration-testing.md
- Writing full request/response tests
- Testing multi-step workflows
- Simulating production scenarios
- Testing WebSocket or streaming responses
Load
when:references/coverage-optimization.md
- Setting up coverage thresholds
- Identifying untested code paths
- Optimizing test suite performance
- Configuring coverage reporters
Load
when:references/troubleshooting.md
- Debugging failing tests
- Resolving binding errors
- Fixing timeout issues
- Understanding error messages
Load
for:templates/vitest-config.ts
- Complete vitest.config.ts example
- Advanced configuration options
- Multiple wrangler environments
Load
for:templates/basic-test.ts
- Test file structure template
- Common test patterns
- beforeEach/afterEach examples
Load
for:templates/binding-mock-test.ts
- Binding-specific test examples
- D1, KV, R2, DO test patterns
- Queue and AI testing examples
Load
for:scripts/setup-vitest.sh
- Automated Vitest installation
- Project configuration script
Load
for:scripts/run-tests.sh
- 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.