@cloudflare/vitest-pool-workers is a custom Vitest pool that runs your tests inside the actual Cloudflare Workers runtime (workerd), providing an authentic testing environment with access to all Workers APIs and bindings.
Installation
npm install -D @cloudflare/vitest-pool-workers
Configuration
defineWorkersConfig()
Define a Vitest config for testing Workers.
// vitest.config.ts
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config" ;
export default defineWorkersConfig ({
test: {
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.toml" },
},
},
} ,
}) ;
config
WorkersUserConfigExport
required
Vitest configuration object Workers-specific pool options Wrangler configuration Environment to use from config
Path to Worker entry point
Whether each test file gets isolated storage
Run all tests in a single Worker instance
Enhanced Vitest configuration with Workers pool settings
defineWorkersProject()
Define a workspace project config for Workers testing.
// vitest.workspace.ts
import { defineWorkersProject } from "@cloudflare/vitest-pool-workers/config" ;
export default [
defineWorkersProject ({
test: {
name: "unit" ,
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.toml" },
},
},
} ,
}),
defineWorkersProject ({
test: {
name: "integration" ,
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.integration.toml" },
isolatedStorage: true ,
},
},
} ,
}),
] ;
Test Runtime API
The cloudflare:test module provides utilities for testing Workers inside the runtime.
env
Access Worker bindings in tests.
import { env } from "cloudflare:test" ;
import { expect , it } from "vitest" ;
it ( "should access KV" , async () => {
await env . MY_KV . put ( "key" , "value" );
const value = await env . MY_KV . get ( "key" );
expect ( value ). toBe ( "value" );
});
it ( "should query D1" , async () => {
const { results } = await env . DB . prepare ( "SELECT * FROM users" ). all ();
expect ( results ). toHaveLength ( 0 );
});
Environment object containing all Worker bindings defined in your Wrangler config:
KV namespaces
D1 databases
R2 buckets
Durable Objects
Service bindings
Queue producers
Environment variables
SELF
Reference to the current Worker for making fetch requests.
import { SELF } from "cloudflare:test" ;
import { expect , it } from "vitest" ;
it ( "should handle requests" , async () => {
const response = await SELF . fetch ( "https://example.com/" );
expect ( response . status ). toBe ( 200 );
expect ( await response . text ()). toBe ( "Hello World" );
});
it ( "should handle POST requests" , async () => {
const response = await SELF . fetch ( "https://example.com/api/users" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({ name: "Alice" }),
});
expect ( response . status ). toBe ( 201 );
});
Fetcher object for the current Worker fetch
(input: RequestInfo, init?: RequestInit) => Promise<Response>
Send a request to the Worker
fetchMock
Mock outbound fetch requests from your Worker.
import { fetchMock } from "cloudflare:test" ;
import { expect , it , beforeAll , afterEach } from "vitest" ;
beforeAll (() => {
fetchMock . activate ();
});
afterEach (() => {
fetchMock . assertNoPendingInterceptors ();
});
it ( "should mock API calls" , async () => {
// Mock a specific endpoint
fetchMock
. get ( "https://api.example.com" )
. intercept ({ path: "/users/1" })
. reply ( 200 , { id: 1 , name: "Alice" });
const response = await SELF . fetch ( "https://example.com/user/1" );
const data = await response . json ();
expect ( data . name ). toBe ( "Alice" );
});
it ( "should mock with functions" , async () => {
fetchMock
. get ( "https://api.example.com" )
. intercept ({ path: "/time" })
. reply ( 200 , () => ({ time: Date . now () }));
});
it ( "should mock errors" , async () => {
fetchMock
. get ( "https://api.example.com" )
. intercept ({ path: "/error" })
. reply ( 500 , { error: "Internal Server Error" });
});
Undici MockAgent instance for mocking fetch requests get
(origin: string) => MockPool
Get mock pool for an origin
assertNoPendingInterceptors
Assert all mocked requests were called
createExecutionContext()
Create an ExecutionContext for testing.
import { env , createExecutionContext , waitOnExecutionContext } from "cloudflare:test" ;
import { expect , it } from "vitest" ;
import worker from "./index" ;
it ( "should use waitUntil" , async () => {
const ctx = createExecutionContext ();
const request = new Request ( "https://example.com/" );
const response = await worker . fetch ( request , env , ctx );
await waitOnExecutionContext ( ctx );
expect ( response . status ). toBe ( 200 );
});
Execution context with waitUntil() and passThroughOnException()
waitOnExecutionContext()
Wait for all waitUntil() promises to complete.
import { createExecutionContext , waitOnExecutionContext } from "cloudflare:test" ;
const ctx = createExecutionContext ();
ctx . waitUntil ( someAsyncOperation ());
await waitOnExecutionContext ( ctx );
Execution context to wait on
Resolves when all waitUntil promises complete
getQueueResult()
Get messages sent to a queue during a test.
import { env , getQueueResult , SELF } from "cloudflare:test" ;
import { expect , it } from "vitest" ;
it ( "should send queue messages" , async () => {
await SELF . fetch ( "https://example.com/action" );
const result = await getQueueResult ( env . MY_QUEUE );
expect ( result . messages ). toHaveLength ( 1 );
expect ( result . messages [ 0 ]. body ). toEqual ({ action: "process" });
});
Queue binding to get results from
Queue result containing sent messages Show QueueResult properties
Array of messages sent to the queue
Testing Helpers
runInDurableObject()
Run code inside a Durable Object instance.
import { env , runInDurableObject } from "cloudflare:test" ;
import { expect , it } from "vitest" ;
import { MyDurableObject } from "./durable-object" ;
it ( "should access DO storage" , async () => {
const id = env . MY_DO . idFromName ( "test" );
await runInDurableObject ( env . MY_DO , id , async ( instance , state ) => {
await state . storage . put ( "key" , "value" );
const value = await state . storage . get ( "key" );
expect ( value ). toBe ( "value" );
});
});
namespace
DurableObjectNamespace
required
Durable Object namespace
fn
(instance: T, state: DurableObjectState) => Promise<R>
required
Function to run with access to the instance and state
Configuration Examples
Basic Setup
// vitest.config.ts
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config" ;
export default defineWorkersConfig ({
test: {
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.toml" },
},
},
} ,
}) ;
With Miniflare Options
// vitest.config.ts
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config" ;
export default defineWorkersConfig ({
test: {
poolOptions: {
workers: {
miniflare: {
// Override bindings for tests
bindings: {
TEST_MODE: true ,
},
kvNamespaces: {
TEST_KV: "test-namespace" ,
},
},
},
},
} ,
}) ;
Isolated Storage
// vitest.config.ts
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config" ;
export default defineWorkersConfig ({
test: {
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.toml" },
// Each test file gets its own isolated storage
isolatedStorage: true ,
},
},
} ,
}) ;
Workspace Projects
// vitest.workspace.ts
import { defineWorkersProject } from "@cloudflare/vitest-pool-workers/config" ;
export default [
defineWorkersProject ({
test: {
name: "unit" ,
include: [ "src/**/*.test.ts" ],
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.toml" },
},
},
} ,
}),
defineWorkersProject ({
test: {
name: "integration" ,
include: [ "test/**/*.test.ts" ],
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.integration.toml" },
isolatedStorage: true ,
},
},
} ,
}),
] ;
Complete Test Example
// worker.test.ts
import { env , SELF , fetchMock , createExecutionContext , waitOnExecutionContext } from "cloudflare:test" ;
import { describe , it , expect , beforeAll , afterEach } from "vitest" ;
import worker from "./index" ;
describe ( "Worker Tests" , () => {
beforeAll (() => {
fetchMock . activate ();
});
afterEach (() => {
fetchMock . assertNoPendingInterceptors ();
});
it ( "should respond to requests" , async () => {
const response = await SELF . fetch ( "https://example.com/" );
expect ( response . status ). toBe ( 200 );
});
it ( "should use KV storage" , async () => {
await env . MY_KV . put ( "test-key" , "test-value" );
const value = await env . MY_KV . get ( "test-key" );
expect ( value ). toBe ( "test-value" );
});
it ( "should query D1" , async () => {
await env . DB . exec ( `
CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO users (name) VALUES ('Alice');
` );
const { results } = await env . DB . prepare ( "SELECT * FROM users" ). all ();
expect ( results ). toHaveLength ( 1 );
expect ( results [ 0 ]. name ). toBe ( "Alice" );
});
it ( "should mock fetch requests" , async () => {
fetchMock
. get ( "https://api.example.com" )
. intercept ({ path: "/data" })
. reply ( 200 , { message: "mocked" });
const response = await SELF . fetch ( "https://example.com/proxy" );
const data = await response . json ();
expect ( data . message ). toBe ( "mocked" );
});
it ( "should use execution context" , async () => {
const ctx = createExecutionContext ();
const request = new Request ( "https://example.com/async" );
const response = await worker . fetch ( request , env , ctx );
await waitOnExecutionContext ( ctx );
expect ( response . status ). toBe ( 200 );
});
});