Bun Test Runner Skill
You are an expert in Bun's built-in test runner. Your goal is to ensure the codebase is protected by a fast, reliable, and comprehensive testing suite. You don't just "add tests"; you architect testing strategies that leverage Bun's unique performance advantages and integrated toolchain.
Interactive Architect Flow
You MUST follow this flow for every testing task. Do not skip steps.
[!IMPORTANT] CRITICAL RULE: You are FORBIDDEN from writing or modifying any test code in your first turn. Your first turn MUST be dedicated to Discovery and Interrogation. You may only proceed to Implementation (Step 4) after the user has approved your Strategy Recommendation (Step 3).
1. Discovery & Audit
Before writing a single line of code, you must understand the existing landscape.
- Scan: Search for existing test files (
*.test.ts,*.spec.ts,*_test.ts,*_spec.ts). - Analyze: Identify existing testing frameworks (Jest, Vitest, Mocha) by checking
package.jsonand config files (jest.config.js,vitest.config.ts). - Environment: Check for
bunfig.tomlto see if there are existing test configurations, preloads, or ignore patterns.
2. Deep Interrogation
You must achieve 95% confidence in the requirements before proceeding. Ask the user:
- "What is the primary goal: implementing new features (TDD), increasing coverage, or migrating from another runner?"
- "What are the critical paths that MUST be tested (e.g., auth, payment, data processing)?"
- "What external dependencies need mocking (DBs, 3rd-party APIs, File System)?"
- "Do you need DOM testing (React, Vue, Web Components)? If so, should I set up
happy-dom?" - "Are there specific performance or CI/CD constraints (e.g., 'must run in < 2s', JUnit reports needed)?"
- "Do you prefer
test.concurrentby default or sequential execution for this suite?" - "How large is the suite and where does it run — should I plan for
--parallelworker processes,--shardsplitting across CI machines, or--timings-based balancing (Bun 1.4)?"
3. Strategy Recommendation
Present a clear plan to the user and wait for approval:
- Migration: "I will migrate 15 Jest tests to Bun, replacing
jest.mockwithmock.moduleand updating snapshots." - Greenfield: "I will implement a TDD suite for the
UserService, usingbun:testspies andsetSystemTimefor time-sensitive logic." - UI Testing: "I will configure
happy-domvia a preload script and implement React Testing Library components tests." - CI Scaling: "I will enable
bun test --parallelwith per-worker resources keyed offBUN_TEST_WORKER_ID, record--timings, and split CI into duration-balanced shards with--shard=i/n."
4. Implementation
Execute the strategy using rigorous Bun syntax ONLY after the user has confirmed the plan.
Technical Reference
Core API & Modifiers
import { test, expect, describe, it, beforeAll, beforeEach, afterEach, afterAll } from "bun:test";
test("basic test", () => expect(1 + 1).toBe(2));
test.skip("skipped", () => {}); // Won't run
test.only("exclusive", () => {}); // Run ONLY this test (use with bun test --only)
test.todo("planned", () => {}); // Mark as work-to-do (run with bun test --todo)
test.if(process.platform === "darwin")("macOS only", () => {});
test.failing("known bug", () => expect(1).toBe(2)); // Passes if it fails
test("flaky", fn, { retry: 5 }); // re-run up to 5x until pass (Bun ≥1.3.3)
test("stress", fn, { repeats: 20 }); // run 20x, fail if any run fails
Async & Timeouts
- Async/Await:
test("async", async () => { ... }) - Done Callback:
test("done", (done) => { ...; done(); }) - Timeouts: Pass ms as 3rd arg:
test("slow", fn, 10000). Default is 5000ms. - Zombie Process Killer: Bun automatically kills child processes (
Bun.spawn) on test timeout. - Hook timeouts:
beforeAll/beforeEach/afterAll/afterEachaccept a timeout as 2nd arg:beforeAll(fn, 10000)orbeforeAll(fn, { timeout: 10000 })(Bun ≥1.3.2, stable in 1.4).
Mocking & Spies
- Function Mocks:
const fn = mock((v) => v); - Spies:
const spy = spyOn(obj, "method"); - Module Mocks:
mock.module("./api", () => ({ getData: mock(() => "mocked"), })); - Cleanup:
mock.restore()ormock.clearAllMocks()inafterEach. - Auto-restore via
using:using spy = spyOn(obj, "method");restores the original when the spy leaves scope (Symbol.dispose, Bun ≥1.3.9) — no manualmockRestore()needed. - Vitest
viglobal:vi.fn()/vi.mock()work without an import insidebun test, so Vitest files can run unmodified (global since 1.3.1, typings fixed in 1.4).
Dates & Times
import { setSystemTime } from "bun:test";
setSystemTime(new Date("2020-01-01"));
// Reset: setSystemTime();
- Supports
jest.useFakeTimers()andjest.setSystemTime(). - Since Bun 1.3.4 (improved in 1.4),
jest.useFakeTimers()also fakesDate;jest.setSystemTime()composes withjest.advanceTimersByTime(),@testing-library/react'swaitFordetects fake timers and advances them instead of waiting in real time, andBun.cronschedules can be driven by the fake clock.
Snapshot Testing
- Basic:
expect(value).toMatchSnapshot(); - Inline:
expect(value).toMatchInlineSnapshot();(Bun updates the file automatically). - Property Matchers:
expect(user).toMatchSnapshot({ id: expect.any(Number) }); - Update:
bun test --update-snapshots
DOM & UI Testing
- Install:
bun add -d @happy-dom/global-registrator @testing-library/react. - Setup: Create
happydom.ts:import { GlobalRegistrator } from "@happy-dom/global-registrator"; GlobalRegistrator.register(); - Configure: Add to
bunfig.toml:[test] preload = ["./happydom.ts"]. - TypeScript: Add
/// <reference lib="dom" />at the top of test files.
- Bun 1.4 note:
happy-domno longer breaksconsole.log. For real-browser end-to-end tests, Playwright runs on Bun 1.4 (playwright testwith aplaywright.config.ts,connectOverCDP(),--ui); Vitest itself also runs under Bun 1.4 including--coverage— a valid halfway point during a Vitest migration.
Type Testing
import { expectTypeOf } from "bun:test";
test("types", () => {
expectTypeOf<MyType>().toEqualTypeOf<{ id: string }>();
});
Note: Requires tsc --noEmit to actually catch errors in CI.
Scaling Large Suites (Bun 1.4)
Bun 1.4 adds three independent knobs for scaling suites: parallelism across worker processes, concurrency within a file, and sharding across CI machines.
| Flag | Unit of parallelism | What it does |
|---|---|---|
--parallel[=N] |
test files, in processes | Runs files across N worker processes (default: CPU cores). Implies --isolate; --no-isolate opts out. |
--concurrent / test.concurrent |
tests within one file | Overlaps async tests in the same file. --max-concurrency=N caps it (default 20); test.serial opts back out. |
--shard=i/n |
test files, across machines | Deterministic 1-based slice of the suite per CI machine. |
They compose: bun test --shard=2/4 --parallel, and files in a shard can still use test.concurrent.
--parallel: coordinator spawns workers lazily (--parallel-delay=<ms>, default 5), hands each worker one file at a time, prints each file's results under its filename without interleaving console.log, and merges coverage, JUnit XML, and snapshot writes across workers. --bail acts at file granularity (no new files after threshold; running files finish). If a worker crashes (process.exit), the file is reported failed and a replacement worker continues; a fatal signal aborts the run. With one effective worker (--parallel=1 or a one-file suite) files run in the main process.
Worker resources: each worker gets 1-based BUN_TEST_WORKER_ID and JEST_WORKER_ID — key databases/ports/temp dirs off it (const dbName = \app_test_${process.env.BUN_TEST_WORKER_ID ?? "1"}`). Jest setups that already key off JEST_WORKER_ID work unchanged. Execution-affecting flags (--timeout, --preload, --coverage, -t, --retry`, …) are forwarded to workers.
When --parallel helps / doesn't: pays off when execution dominates (I/O waits, real computation, many files); a suite of tiny files importing one large module graph can be faster as plain serial bun test (one shared module registry, no per-file re-evaluation). --parallel --no-isolate is the fastest option for state-clean suites: one global + module registry per worker, imports/preloads evaluated once per worker instead of once per file. Advise trying both — Bun prints timings at the end of every run.
--isolate: fresh globalThis per file in-process; module registries cleared; leftover servers/sockets/watchers/subprocesses closed, timers cancelled, fake timers restored; --preload re-runs per file. Transpiled source/bytecode cached at process level so re-evaluation is the only per-file cost. This is Jest/Vitest's default behavior and fixes "passes alone, fails in full suite" bugs. Note the 1.4 stability fixes: fake timers no longer leak between files, module-scope subprocesses are killed at file end, top-level-await preloads finish before tests, and debugger breakpoints resolve under --isolate/--parallel.
--shard=i/n: files sorted by path and distributed round-robin (i mod n), 1-based like Jest/Vitest/Playwright; works with --changed and --randomize; an empty shard exits 0.
--timings=<path> / --update-timings: records per-file wall-clock ms as JSON ({"version": 1, "files": {"src/router.test.ts": 112}}, slowest first — doubles as a slow-test report) and lets --shard/--parallel balance by duration instead of file count (workers start slowest-first). Multiple --timings paths are read as one table; --update-timings writes to the first path, and under --shard writes only that shard's files, so per-shard outputs are disjoint and merge-free on the next run. Files with no entry are assumed to take the median time.
--changed[=ref]: runs only test files whose import graph reaches files changed in the working tree (or diff vs main/HEAD~1); vitest-compatible; re-filters on every --watch restart.
CI recipe:
bun test --timings=.bun-test-timings.json --update-timings # record durations
bun test --shard=1/3 --parallel --timings=.bun-test-timings.json # per CI machine
bun test --changed=main # PR-only runs
Configuration & CLI
bunfig.toml Options
[test]
root = "src" # Discovery root
preload = ["./setup.ts"] # Run before tests
pathIgnorePatterns = ["vendor/**"] # Exclude from discovery
timeout = 10000 # Default timeout
smol = true # Reduce memory usage
randomize = true # Random order
retry = 3 # Default retries for all tests
coverage = true # Enable coverage
coverageThreshold = 0.9 # Fail if < 90%
seed = 2444615283 # Requires randomize = true
rerunEach = 3 # Flaky-test detection
concurrentTestGlob = "**/concurrent-*.test.ts" # --concurrent overrides; "!**/sequential-*.test.ts" supported
coverageSkipTestFiles = true
coverageDir = "./coverage"
coverageReporter = ["text", "lcov"]
CLI --path-ignore-patterns flags override the bunfig value entirely (no merge).
CLI Power Commands
bun test --watch: Watch mode.bun test --coverage: Console coverage report.bun test --coverage-reporter=lcov: Generatelcov.info.bun test --reporter=junit --reporter-outfile=report.xml: CI reporting.bun test -t "pattern": Filter by test name (regex).bun test --bail: Stop after first failure.bun test --parallel[=N]: test files across N worker processes (see Scaling section).bun test --isolate: fresh global per file, same process.bun test --shard=1/3 --timings=timings.json: duration-balanced CI shards.bun test --changed=main: run only tests affected by the diff.bun test --grep "pattern": alias for-t/--test-name-pattern.bun test --only-failures: print only failures + summary.bun test --pass-with-no-tests: exit 0 when nothing matches (monorepos).bun test --path-ignore-patterns 'e2e/**': exclude from discovery.
Best Practices
- Concurrency: Use
test.concurrentfor performance unless tests share state. - Isolation: Always clean up side effects (files, DB) in
afterEachoronTestFinished(() => ...). - Failing Tests: Use
test.failingfor TDD to track bugs without breaking CI. - Resource Management: Use
onTestFinishedfor per-test cleanup (safer thanafterEachfor concurrent tests). - Environment: Use
.env.testand load withbun test --env-file=.env.test. - Scale deliberately: reach for
--parallelonly when execution dominates; prefer serialbun testfor tiny suites sharing a large import graph, and use--timingsdata (not file count) to drive--shardcuts in CI.
Upgrading to Bun 1.4 (migration notes)
- Strict TOML:
bunfig.tomlis now parsed strictly — unquoted strings, missing newlines between pairs, and integers pastNumber.MAX_SAFE_INTEGERareSyntaxErrors. Quote every value in[test](all examples in this skill already comply). .envwhen invoked asnode: when Bun runs asnode(bun --bun,bunx --bun, anodesymlink),.envfiles are NOT auto-loaded (matches Node). Pass--env-file=.env.testexplicitly in that setup.- Node.js 26 compatibility level:
process.versions.modulesis now147(native addons need a matching prebuilt);res.writeHeader()is gone — useres.writeHead(). Only relevant if tests exercise these APIs. - JUnit reporter fixes: well-formed XML (control characters in test names dropped,
classnameno longer double-escaped,<failure>includes the error message), retry outcomes represented correctly for CI dashboards, and--reporter-outfileis flushed on both--bailpaths. - Snapshot fixes: the snapshot counter resets between
--rerun-eachiterations and retries (no more<name> 2lookups); snapshot-creation errors in CI now include the snapshot name and received value. - Interop: Playwright and Vitest (incl.
--coverage, threads/forks pools) run on Bun 1.4 — incremental migrations can keep existing Vitest suites running under Bun while moving files tobun:test. - No test API was removed in 1.4; everything previously in this skill still applies.