--parallel flag for test file parallelism
The --parallel flag runs test files across N worker processes (default: number of CPU cores). It implies --isolate unless --no-isolate is specified. Usage: 'bun test --parallel' for one worker per CPU core, or 'bun test --parallel=4' for exactly 4 workers.
--concurrent flag for concurrent tests within a file
The --concurrent flag or test.concurrent() lets async tests in the same file overlap while one is awaiting. This is cooperative concurrency for I/O-bound tests, not extra CPU cores. Individual tests or whole describe groups can be marked with test.concurrent(). Use test.serial to opt out of --concurrent. Default max concurrency is 20; it can be capped with --max-concurrency=N.
--shard flag for splitting suite across CI machines
The --shard=i/n flag runs the i-th of n deterministic slices of the suite. Every machine sorts discovered test files by path and takes a deterministic slice, so together the shards cover each file exactly once with no coordination. Without --timings, file i of the sorted list goes to shard (i mod n) + 1 — balanced by file count.
--isolate flag for fresh global per file
The --isolate flag runs each test file in a fresh JavaScript global object inside the same process. Between files Bun creates a new globalThis, clears the ESM and CommonJS module registries, closes servers and sockets, cancels timers, and re-runs --preload scripts in the new global. --parallel implies --isolate unless --no-isolate opts out. Transpiled source and bytecode are cached at process level and shared across globals.
Three independent parallelism knobs for bun test
bun test has three independent knobs for running multiple things at once: --parallel (test files in processes), --concurrent / test.concurrent (tests within one file), and --shard (test files across machines). They compose: a CI job can run 'bun test --shard=2/4 --parallel' and files in that shard can still contain test.concurrent tests.
Worker environment variables for --parallel
Each worker in a --parallel run gets BUN_TEST_WORKER_ID and JEST_WORKER_ID environment variables set to its 1-based index, so tests can pick a distinct database, port range, or temp directory per worker.
File distribution strategy in --parallel
Files are sorted by path and split into one contiguous chunk per worker, so files in the same directory usually land in the same process. When a worker drains its chunk it steals the back half of the largest remaining chunk. With --timings, chunks are cut by recorded duration instead of file count, each worker starts its slowest file first, and an idle worker steals the slowest not-yet-started file.
Worker lazy start with --parallel-delay
Workers start lazily in --parallel mode. The first worker starts immediately; the rest are only spawned once every running worker has been busy for a few milliseconds. The delay is controlled by --parallel-delay (default 5ms). A suite of tiny files therefore runs on a single worker with no process-spawn overhead, while the first slow file triggers full fan-out.
Flags forwarded to --parallel workers
Flags that affect how tests execute (--timeout, --preload, --define, --coverage, --update-snapshots, -t, --retry, --rerun-each, --concurrent, --randomize/--seed, and others) are forwarded to workers. --bail is handled by the coordinator at file granularity: once the failure threshold is reached no new files are started, but files already running finish.
Worker crash handling in --parallel
If a worker crashes (a native addon segfaults, or a test calls process.exit), the file it was running is reported as failed and a replacement worker picks up the remaining files. A crash from a fatal signal aborts the whole run so it can't be masked by later passing files.
When --parallel helps vs. when it doesn't
The --parallel flag pays off when the suite is dominated by test execution — I/O waits, computation, subprocesses, many files. It costs something: every file re-evaluates its imports in a fresh global and each worker is a separate process with its own JIT warm-up. For a suite of very fast files that all import the same large module graph, plain 'bun test' can be faster.
Concurrent tests share thread and global
Concurrent tests marked with test.concurrent() share a thread and a global object. This is cooperative concurrency for I/O-bound tests, not extra CPU cores. expect.assertions() and other per-test global state need care under concurrency.
concurrentTestGlob configuration option
The concurrentTestGlob option in bunfig.toml can turn on --concurrent for matching files only, providing file-level configuration of concurrent test execution.
Default isolation behavior without --isolate
Without --isolate (the default), all files share one global and one module registry. This is the fastest mode and is fine for suites whose files don't leak state into each other.
How Bun caches transpiled source with --isolate
To keep the cost of --isolate low, transpiled source and bytecode are cached at the process level and shared across globals: the second file to import a module skips reading, transpiling and parsing it and goes straight to evaluation. Only the module's top-level code runs again.
Timings file format for --timings
The timings file format is plain JSON with a version field and a files object containing file paths as keys and wall-clock milliseconds for the whole file as values, listed slowest first. Example: {"version": 1, "files": {"test/integration/build.test.ts": 41234, "test/db/migrate.test.ts": 9876}}. Paths are relative to the project root.
Default concurrent test limit
The default maximum number of concurrent tests running at once is 20, and can be capped with --max-concurrency=N.
Concurrent test example
Example of concurrent tests: test.concurrent("GET /users", async () => { const res = await fetch(`${baseUrl}/users`); expect(res.status).toBe(200); }); These async tests overlap while one is awaiting. test.serial marks individual tests to run after the concurrent group, alone.
How Bun approaches parallelism compared to Jest and Vitest
Bun offers performance advantages through multiple parallelism knobs (--parallel, --concurrent, --shard) that can be combined. With a fresh global per file, Bun shares transpiled source and bytecode across globals so nothing is re-parsed, but module evaluation and JIT warm-up still repeat. In benchmarks, 'bun test --parallel --no-isolate' is fastest (0.75s for 2000 files), followed by single-process 'bun test' (2.4s), then 'bun test --parallel' (6.8s), outperforming comparable Vitest and Jest configurations.
JUnit reporter configuration in bunfig.toml
The JUnit reporter can be configured in bunfig.toml under [test.reporter] section with the key `junit = "path/to/junit.xml"` to specify the output path for the JUnit XML report.
bun test --conditions flag for package.json conditions
The --conditions flag sets package.json conditions for module resolution. Example: bun test --conditions development
Hot reloading with --hot flag
The --hot flag is similar to --watch but more aggressive about preserving state between runs. For most tests, --watch is recommended as it provides better isolation between runs.
bun test --prefer-offline and --frozen-lockfile flags
The --prefer-offline and --frozen-lockfile flags affect any network requests or auto-installs during test execution.
Watch mode with --watch flag
The --watch flag enables watch mode, where the test runner watches for file changes and re-runs tests.
Global test functions available without import
The following are available globally in test files without importing: test, describe, it (alias for test), beforeAll, beforeEach, afterAll, afterEach, expect, jest, and vi.
bun test exit codes
bun test uses standard exit codes: 0 means all tests passed with no unhandled errors, and 1 means test failures or unhandled errors occurred.
Signal handling for test runner
The test runner handles common signals: SIGTERM gracefully stops test execution, and SIGKILL immediately stops test execution.
GitHub Actions and CI environment detection
Bun automatically detects certain environments: when process.env.GITHUB_ACTIONS is set, Bun automatically emits GitHub Actions annotations. When process.env.CI is set, certain behaviors may be adjusted for CI environments.
Tests run in single process by default
The test runner runs all tests in a single process by default, providing faster startup with no need to spawn multiple processes, efficient resource usage through shared memory, and simple debugging. However, tests share global state (requiring cleanup with lifecycle hooks), one test crash can affect others, and there is no true parallelization of individual tests.
NODE_ENV defaults to 'test' in bun test
bun test sets $NODE_ENV to 'test' unless it is already set in the environment or in .env files. This can be overridden by explicitly setting NODE_ENV when running bun test.
bun test --env-file flag for environment variables
The --env-file flag loads environment variables for tests. Example: bun test --env-file .env.test
Default test timeout is 5000ms
Each test has a default timeout of 5000ms (5 seconds). Tests that exceed this timeout fail.
Global timeout flag for tests
Change the timeout globally for all tests using the --timeout flag with bun test, specifying the timeout in milliseconds. Example: bun test --timeout 10000 sets a 10 second timeout.
Per-test timeout as third argument
Set a per-test timeout by passing a number as the third argument to the test function. Example: test('name', () => {}, 1000) sets a 1 second timeout for that test.
Disable timeout with 0 or Infinity
Pass 0 or Infinity as the timeout value to a test to disable the timeout and allow it to run indefinitely.
Unhandled errors cause non-zero exit code
bun test tracks unhandled promise rejections and errors that occur between tests. If any occur, the final exit code is non-zero, even if all tests pass. This catches errors in asynchronous code that might otherwise go unnoticed.
bun test memory flag --smol
The --smol flag reduces memory usage for the test runner VM.
bun test debugging flags --inspect and --inspect-brk
The --inspect flag attaches the debugger to the test runner process. The --inspect-brk flag does the same but breaks at startup.
bun test --preload flag for setup scripts
The --preload flag runs scripts before test files, which is useful for global setup and mocks. Example: bun test --preload ./setup.ts
bun test --define flag for compile-time constants
The --define flag sets compile-time constants. Example: bun test --define "process.env.API_URL='http://localhost:3000'"
bun test --tsconfig-override flag
The --tsconfig-override flag allows using a different tsconfig file. Example: bun test --tsconfig-override ./test-tsconfig.json
bun test --loader flag for file extension mapping
The --loader flag maps file extensions to built-in loaders. Example: bun test --loader .svg:text
Verify at least one assertion with expect.hasAssertions()
Use expect.hasAssertions() to verify that at least one assertion is called during a test. This is especially useful in async tests to ensure assertions run.
Verify exact assertion count with expect.assertions(count)
Use expect.assertions(count) to verify that a specific number of assertions are called during a test. This helps ensure all assertions run, especially in complex async code.
Type testing with expectTypeOf
Bun includes expectTypeOf for testing TypeScript types, compatible with Vitest. These are no-ops at runtime; run TypeScript separately with 'bunx tsc --noEmit' to verify type checks.
Basic matchers in Bun
Bun implements: .not, .toBe(), .toEqual(), .toBeNull(), .toBeUndefined(), .toBeNaN(), .toBeDefined(), .toBeFalsy(), .toBeTruthy(), .toStrictEqual().
String and array matchers in Bun
Bun implements: .toContain(), .toHaveLength(), .toMatch(), .toContainEqual(), .stringContaining(), .stringMatching(), .arrayContaining().
Object matchers in Bun
Bun implements: .toHaveProperty(), .toMatchObject(), .toContainAllKeys(), .toContainValue(), .toContainValues(), .toContainAllValues(), .toContainAnyValues(), .objectContaining().
Number matchers in Bun
Bun implements: .toBeCloseTo(), .closeTo(), .toBeGreaterThan(), .toBeGreaterThanOrEqual(), .toBeLessThan(), .toBeLessThanOrEqual().
Function and class matchers in Bun
Bun implements: .toThrow(), .toBeInstanceOf().
Promise matchers in Bun
Bun implements: .resolves(), .rejects().
Mock function matchers in Bun
Bun implements: .toHaveBeenCalled(), .toHaveBeenCalledTimes(), .toHaveBeenCalledWith(), .toHaveBeenLastCalledWith(), .toHaveBeenNthCalledWith(), .toHaveReturned(), .toHaveReturnedTimes(), .toHaveReturnedWith(), .toHaveLastReturnedWith(), .toHaveNthReturnedWith().
Utility matchers in Bun
Bun implements: .extend, .anything(), .any(), .assertions(), .hasAssertions().
Snapshot matchers in Bun
Bun implements: .toMatchSnapshot(), .toMatchInlineSnapshot(), .toThrowErrorMatchingSnapshot(), .toThrowErrorMatchingInlineSnapshot().
addSnapshotSerializer not yet implemented
.addSnapshotSerializer() is not yet implemented in Bun.
Bun aims for complete Jest compatibility
Long term, Bun aims for complete Jest compatibility. Currently, a limited set of expect matchers is supported. See the tracking issue at github.com/oven-sh/bun/issues/1825 for full compatibility status.
Import test utilities from bun:test
Test utilities are imported from the built-in 'bun:test' module, including test, expect, describe, beforeEach, afterEach, and other test functions.
Basic test structure
A basic test is defined by calling test() with a test name string and a callback function. The callback receives the test context and should contain expect() assertions.
Group tests with describe blocks
Group related tests using describe(). Multiple test() calls can be nested within a describe() to organize tests into suites.
Per-test timeout in milliseconds
Pass a number as the third argument to test() to specify a per-test timeout in milliseconds. Example: test('wat', async () => { ... }, 500) sets a 500ms timeout.