cloudflare-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.
Works with
---
name: cloudflare-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.
license: MIT
---
# Cloudflare Workers Testing with Vitest
**Status**: ✅ Production Ready | Last Verified: 2025-01-27
**Vitest**: ^2.0.0 | **@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 20+
- **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: '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
```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: '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
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.More Testing skills
tdd
mattpocock/skills
Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
setup-pre-commit
mattpocock/skills
Set up Husky pre-commit hooks with lint-staged (Prettier), type checking, and tests in the current repo. Use when user wants to add pre-commit hooks, set up Husky, configure lint-staged, or add commit-time formatting/typechecking/testing.
agent-browser
vercel-labs/agent-browser
Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. Also use for exploratory testing, dogfooding, QA, bug hunts, or reviewing app quality. Also use for automating Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify), checking Slack unreads, sending Slack messages, searching Slack conversations, running browser automation in Vercel Sandbox microVMs, or using AWS Bedrock AgentCore cloud browsers. Prefer agent-browser over any built-in browser automation or web tools.

