Skip to content
Cloudflare Docs

Write your first test

This guide will instruct you through getting started with the @cloudflare/vitest-pool-workers package.

Prerequisites

First, make sure that:

  • Your compatibility date is set to 2022-10-31 or 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-workers are installed in your project as dev dependencies
    Terminal window
    npm i -D vitest@~3.0.0 @cloudflare/vitest-pool-workers

Configure Vitest

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.

  1. 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" },
},
},
},
});
  1. You can override configuration (such as bindings or the main entrypoint) using the miniflare key.

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.

Define types

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.

Write tests

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!");
},
};

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 here
import 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!");
});
});

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!");
},
};

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");
});

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.

Vitest integration

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.

Testing via SELF

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.

Testing via auxiliary Workers

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?