npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bun-test-cloudflare

v0.0.12

Published

Run Cloudflare Workers tests in Bun

Readme

bun-test-cloudflare

npm version

Bun test support and a typed harness wrapper for Cloudflare Workers projects, with runtime compatibility patches for running Wrangler test servers under Bun.

What It Provides

  • bun-test-cloudflare/setup: Bun test preload that patches the runtime pieces Wrangler/Miniflare needs under Bun.
  • bun-test-cloudflare: createCloudflareHarness() wrapper that turns named worker config into typed worker handles.

The setup installs only the Bun/Miniflare compatibility patches still needed for the active Bun version. It also provides a minimal cloudflare:workers DurableObject shim for plain Bun module imports.

Version gates use Bun.semver.satisfies with the full bun --revision value, including prerelease identifiers. Bun >=1.4.2 additionally disables worker-threads-fifo and worker-threads-no-timeouts. The FormData override remains enabled to match Miniflare’s Request/Response implementations. Prerelease and invalid versions keep the patches enabled unless explicitly disabled through BUN_TEST_CLOUDFLARE_DISABLED_PATCHES; build metadata does not affect version matching.

Install

bun add -d bun-test-cloudflare

Wrangler Compatibility

bun-test-cloudflare requires wrangler >= 4.104.0.

Configure Bun

Preload the setup before app-specific test setup:

[test]
preload = ["bun-test-cloudflare/setup", "./src/tests/setup.ts"]

If you do not need app-specific setup, use only:

[test]
preload = ["bun-test-cloudflare/setup"]

Create A Typed Harness

Create one test harness module for your package and export the configured harness:

// src/tests/harness.ts
import { createCloudflareHarness, typeToken } from "bun-test-cloudflare";
import path from "node:path";

type BackendBindings = {
  IMAGES_BUCKET: R2Bucket;
};

const packageRoot = path.resolve(import.meta.dir, "../..");

export const harness = createCloudflareHarness({
  workers: {
    BACKEND: {
      bindings: typeToken<BackendBindings>(),
      configPath: path.join(packageRoot, "wrangler.toml"),
      name: "my-backend-worker",
      vars: {
        APP_ENV: "test",
      },
    },
    CMS: {
      configPath: path.join(packageRoot, "../cms/wrangler.toml"),
      name: "my-cms-worker",
      secrets: {
        PAYLOAD_SECRET: "test",
      },
    },
  },
});

export type TestWorkers = Parameters<Parameters<typeof harness.run>[0]>[0];

The object keys become the typed worker handles passed to run(). The optional bindings token is type-only metadata for worker.getEnv() and is not passed to Wrangler.

When a worker uses configPath, bun-test-cloudflare reads that Wrangler config, injects define["process.env.NODE_ENV"] = "'test'", and runs wrangler deploy --dry-run --outdir once for that harness. Build output is written to node_modules/.btcf/worker-build/<worker-name>/worker.js. Test runs then use that script with no_bundle = true, so Wrangler does not rerun its esbuild bundle step for every run().

Use In Tests

import { expect, test } from "bun:test";
import { harness } from "./harness";

test("calls the backend worker", async () => {
  await harness.run(async (workers) => {
    const response = await workers.BACKEND.fetch("https://example.com/health");

    expect(response.status).toBe(200);
  });
});

run() leases a prewarmed Wrangler test server, resets its persistent storage before reuse, and returns it to the pool after the callback. The pool stays alive until Bun's suite cleanup. Set BUN_TEST_CLOUDFLARE_DISABLE_SERVER_PREWARM=1 to create and close a server for every run().

Isolated Worker slots

For harnesses that use supported storage, value, Images, internal service, and Durable Object bindings, isolatedWorkerSlots can amortize one workerd startup across multiple test runs while preserving Worker global and storage isolation:

const harness = createCloudflareHarness({
  workers: {
    BACKEND: { configPath: "wrangler.toml" },
  },
});

Eligible harnesses use four isolated slots by default. Each slot is a distinct Worker service with namespaced Cache API access and unique D1, KV, and R2 storage identities. A slot is leased once. After every slot in a workerd generation has been consumed, the harness rebuilds that generation in the background. Harnesses with unsupported bindings automatically use the single-slot reset path; explicitly requesting multiple slots with unsupported bindings throws.

prewarmedWorkerdPoolSize defaults to 1, avoiding duplicate startup and memory costs. Set it to 2 or higher only when tests need concurrent harness leases; another ready pool member can then serve leases while a generation rebuilds.

Use the workers handles, server.fetch(), or server.getWorker() supplied to run() so requests follow the active slot. A URL returned by server.listen() addresses the generation's first physical Worker and should not be used with slotted harnesses.

Profile Harness Time

Set BUN_TEST_CLOUDFLARE_TIMINGS=1 to print phase timings for Worker startup, lease acquisition, callback execution, storage reset, and cleanup. Fixture tests also forward their captured Worker timing logs in this mode.

BUN_TEST_CLOUDFLARE_TIMINGS=1 bun test

Binding Fixture Coverage

The binding fixture exercises locally simulated value and secret bindings, Analytics Engine, Assets, D1, Durable Objects, Email, Hyperdrive, KV, Queues, R2, Rate Limiting, service bindings, version metadata, and Workflows through createCloudflareHarness(). Browser Rendering, Images, Cache API, and Wasm have dedicated fixtures because they require specialized lifecycle or payload coverage.

The same suite catalogs every binding kind emitted by Wrangler's config converter and asserts whether isolated Worker slots may use it. Binding kinds without a deterministic local simulator are configuration-tested and must fall back to the single-slot reset path. When Wrangler adds a new kind, the catalog test fails until its isolation behavior is explicitly classified.

OpenNext Applications

Build the Next application with opennextjs-cloudflare build before creating the harness, then use the application's normal wrangler.toml. Relative worker and asset paths, including .open-next/assets, are resolved from that config file.

import { createCloudflareHarness } from "bun-test-cloudflare";
import path from "node:path";

const appRoot = path.resolve(import.meta.dir, "..");

const harness = createCloudflareHarness({
  workers: {
    APP: {
      configPath: path.join(appRoot, "wrangler.toml"),
      name: "my-opennext-app",
    },
  },
});

await harness.run(async (workers) => {
  const response = await workers.APP.fetch("https://example.test/");
  expect(await response.text()).toContain("Expected rendered content");
});

Lifecycle Events

Use events.beforeRun for per-run setup after Wrangler has started and before the test callback runs:

const harness = createCloudflareHarness({
  events: {
    beforeRun: async (workers) => {
      const env = await workers.BACKEND.getEnv();
      await env.DB.prepare("SELECT 1").run();
    },
  },
  workers: {
    BACKEND: { configPath: "./wrangler.toml" },
  },
});

Worker Names

createCloudflareHarness() uses each worker config's name when calling Wrangler's server.getWorker(name). If name is omitted, it falls back to the object key:

const harness = createCloudflareHarness({
  workers: {
    BACKEND: { configPath: "./wrangler.toml" },
  },
});

await harness.run(async (workers) => {
  await workers.BACKEND.fetch("https://example.com");
});

Direct Server Access

The current Wrangler server is available inside run() when needed:

await harness.run(async (workers, server) => {
  const logs = server.getLogs();
  const env = await workers.BACKEND.getEnv();
  await env.IMAGES_BUCKET.put("fixture.png", new Uint8Array());
});

Access The Active Run Context

Code called inside harness.run() can read the active workers and server without threading them through every helper:

import { getCloudflareHarnessRunContext } from "bun-test-cloudflare";

export async function createFixture() {
  const { workers } = getCloudflareHarnessRunContext<{
    BACKEND: { configPath: string; name: string };
  }>();
  const env = await workers.BACKEND.getEnv();

  await env.MY_BUCKET.put("fixture.txt", "hello");
}

The run context is backed by AsyncLocalStorage, so it is scoped to the current harness.run() callback and async work started from it. Calling getCloudflareHarnessRunContext() outside harness.run() throws.