Test a Worker inside workerd with the Vitest plugin, not in Node

@cloudflare/vitest-plugin runs Vitest inside workerd with your real bindings; import env and exports from cloudflare:workers and call exports.default.fetch().

Workers

· Chapter

29

·

3

min read

The answer. Running Worker code under Node with mocks tests a runtime you do not ship. The Vitest integration runs the tests themselves inside workerd, so Request, Response, streams, crypto and every binding behave exactly as in production, and each test file gets isolated storage. The package is @cloudflare/vitest-plugin (renamed from @cloudflare/vitest-pool-workers in August 2026): a Vite plugin, cloudflareTest(), that reads your Wrangler config. In tests, import { env, exports } from "cloudflare:workers"; exports.default.fetch(url) drives the whole Worker, env.DB or env.KV reach bindings directly. Work queued with ctx.waitUntil is invisible until you wait for it.

The pattern.

// vitest.config.ts
import { cloudflareTest, readD1Migrations } from "@cloudflare/vitest-plugin";
import { defineConfig } from "vitest/config";
export default defineConfig({
  plugins: [cloudflareTest(async () => ({
    wrangler: { configPath: "./wrangler.jsonc" },
    miniflare: { bindings: { TEST_MIGRATIONS: await readD1Migrations("./migrations") } },
  }))],
  test: { setupFiles: ["./test/apply-migrations.ts"] },
});
// test/images.spec.ts
import { env, exports } from "cloudflare:workers";
import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test";
it("POST then GET", async () => {
  const post = await exports.default.fetch("https://x/images", { method: "POST", body: JSON.stringify({ url: "https://a/b.png", author: "me" }) });
  expect(post.status).toBe(201);
  const rows = await env.DB.prepare("select count(*) as n from images").first<{ n: number }>();
  expect(rows?.n).toBe(1);
});

Watch out.

  • Any URL works for fetch() in tests, but it must be absolute; the host is never resolved.
  • Storage isolation is per file. Tests inside one file share state, so a POST-then-GET pair depends on order.
  • Outbound fetch() to third parties is real unless you intercept it; use @msw/cloudflare rather than hand-rolled globals.

Related: d1-migrations-prepared-statements-and-batch · wrangler-dev-local-by-default-remote-bindings · n8n-code-nodes-compiled-with-parameter-hash-contract