Write your first test
This guide will instruct you through getting started with the @cloudflare/vitest-pool-workers package.
First, make sure that:
- Your compatibility date is set to
2022-10-31or later - Your Worker using the ES modules format (if not, refer to the migrate to the ES modules format guide)
- Vitest and
@cloudflare/vitest-pool-workersare installed in your project as dev dependenciesTerminal window npm i -D vitest@~3.0.0 @cloudflare/vitest-pool-workersTerminal window pnpm add -D vitest@~3.0.0 @cloudflare/vitest-pool-workersTerminal window yarn add -D vitest@~3.0.0 @cloudflare/vitest-pool-workers
Create a vitest.config.js or vitest.config.ts file if you do not already have one.
Then add the following snippet, which uses defineWorkersConfig to configure the Workers Vitest integration.
There are two ways to configure the Workers Vitest integration.
- If you want to use the same configuration as your Worker, you can simply reference a Wrangler file via
wrangler.configPath.
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: "./wrangler.toml" }, }, }, },});- You can override configuration (such as bindings or the
mainentrypoint) using theminiflarekey.
For example, this configuration would add a KV namespace TEST_NAMESPACE that was only accessible in tests. Using this method, you can add or override existing bindings like Durable Objects or service bindings.
// maybe mention mocking things (e.g. as a service binding) and link to a separate page.
export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: "./wrangler.toml" }, main: "./dist/index.js", miniflare: { kvNamespaces: ["TEST_NAMESPACE"], }, }, }, },});Under the hood, the Workers Vitest integration uses Miniflare ↗, the same simulator that powers wrangler dev's local mode. Options can be passed directly to Miniflare for advanced configuration.
If you are using TypeScript, you will need to define types for Cloudflare Workers and cloudflare:test to make sure they are detected appropriately. Add a tsconfig.json in the same folder as your tests and add the following:
{ "extends": "../tsconfig.json", "compilerOptions": { "moduleResolution": "bundler", "types": [ "@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers" ] }, "include": ["./**/*.ts", "../src/env.d.ts"]}Save this file, and you are ready to write your first test.
TODO: add note about env.d.ts file and ProvidedEnv.
This example Worker receives a request, and returns a response of "Hello World!".
export default { async fetch(request, env, ctx) { if (pathname === "/404") { return new Response("Not found", { status: 404 }); } return new Response("Hello World!"); },};export default { async fetch(request, env, ctx): Promise<Response> { return new Response("Hello World!"); },} satisfies ExportedHandler<Env>;Unit test example
In order to test this, create a test folder with the following test file:
import { env, createExecutionContext, waitOnExecutionContext,} from "cloudflare:test";import { describe, it, expect } from "vitest";// Could import any other source file/function hereimport worker from "../src";
describe("Hello World worker", () => { it("responds with Hello World!", async () => { const request = new Request("http://example.com"); // Create an empty context to pass to `worker.fetch()` const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions await waitOnExecutionContext(ctx); expect(await response.text()).toBe("Hello World!"); });});import { env, createExecutionContext, waitOnExecutionContext,} from "cloudflare:test";import { describe, it, expect } from "vitest";// Could import any other source file/function hereimport worker from "../src";
// For now, you'll need to do something like this to get a correctly-typed// `Request` to pass to `worker.fetch()`.const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
describe("Hello World worker", () => { it("responds with Hello World!", async () => { const request = new IncomingRequest("http://example.com"); // Create an empty context to pass to `worker.fetch()` const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions await waitOnExecutionContext(ctx); expect(await response.text()).toBe("Hello World!"); });});Add functionality to handle a 404 path on the Worker. This functionality will return the text Not found as well as the status code 404.
export default { async fetch(request, env, ctx) { const { pathname } = new URL(request.url);
if (pathname === "/404") { return new Response("Not found", { status: 404 }); }
return new Response("Hello World!"); },};export default { async fetch(request, env, ctx): Promise<Response> { const { pathname } = new URL(request.url);
if (pathname === "/404") { return new Response("Not found", { status: 404 }); }
return new Response("Hello World!"); },} satisfies ExportedHandler<Env>;To test this, add the following to your test file:
it("responds with not found and proper status for /404", async () => { const request = new Request("http://example.com/404"); // Create an empty context to pass to `worker.fetch()` const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions await waitOnExecutionContext(ctx); expect(await response.status).toBe(404); expect(await response.text()).toBe("Not found");});it("responds with not found and proper status for /404", async () => { const request = new IncomingRequest("http://example.com/404"); // Create an empty context to pass to `worker.fetch()` const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions await waitOnExecutionContext(ctx); expect(await response.status).toBe(404); expect(await response.text()).toBe("Not found");});Integration tests test multiple units of your Worker together by sending HTTP requests to your Worker and asserting on the HTTP responses. As an example, consider the following Worker:
export function add(a, b) { return a + b;}
export default { async fetch(request) { const url = new URL(request.url); const a = parseInt(url.searchParams.get("a")); const b = parseInt(url.searchParams.get("b")); return new Response(add(a, b)); },};An integration test for this Worker might look like the following example:
// Start Worker HTTP server on port 8787 running `index.mjs` then...const response = await fetch("http://localhost:8787/?a=1&b=2");
assert((await response.text()) === "3");In the above example, instead of importing the add function as a unit test would do, you make a direct call to the endpoint, testing that the Worker responds at the endpoint with the appropriate response.
The recommended way to write integration tests for your Workers is by using the Workers Vitest integration. Vitest can be configured to run integrations against a single Worker or multiple Workers.
If testing a single Worker, you can use the SELF fetcher provided by the cloudflare:test API.
import { SELF } from "cloudflare:test";
it("dispatches fetch event", async () => { const response = await SELF.fetch("https://example.com"); expect(await response.text()).toMatchInlineSnapshot(...);});When using SELF for integration tests, your Worker code runs in the same context as the test runner. This means you can use global mocks to control your Worker, but also means your Worker uses the same subtly different module resolution behavior provided by Vite.
Usually this is not a problem, but if you would like to run your Worker in a fresh environment that is as close to production as possible, using an auxiliary Worker may be a good idea. Auxiliary Workers have some developer experience (DX) limitations.
It is also possible to configure Workers for integration testing via vitest.config.ts. An example vitest.config.ts configuration file ↗ on GitHub.
The Worker can then be referenced like the following example:
import { env } from "cloudflare:test";import { expect, it } from "vitest";
it("dispatches fetch event", async () => { const response = await env.WORKER.fetch("http://example.com"); expect(await response.text()).toBe("👋");});Instead of running the Worker-under-test in the same Worker as the test runner like SELF, this example defines the Worker-under-test as an auxiliary Worker. This means the Worker runs in a separate isolate to the test runner, with a different global scope. The Worker-under-test runs in an environment closer to production, but Vite transformations and hot-module-reloading aren't applied to the Worker—you must compile your TypeScript to JavaScript beforehand.
Auxiliary Workers cannot be configured from Wrangler files. You must use Miniflare WorkerOptions ↗ in vitest.config.ts.
common issues section? node js compat?
-
api reference
-
mocking?
-
recipes
-
@cloudflare/vitest-pool-workersGitHub repository ↗ - Examples of tests using the@cloudflare/vitest-pool-workerspackage.
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Products
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark