Open-Source Wikis

/

Bun

/

Apps

/

Test runner

oven-sh/bun

Test runner

Active contributors: Jarred Sumner, Dylan Conway

bun test is a Jest-compatible test runner built into the runtime. The dispatch entry is src/cli/test_command.zig (~103 KB). It shares the runtime's VM, parser, and module loader.

Purpose

  • Discover *.test.ts/*.test.tsx/*.test.js/... files.
  • Spawn one process per test file (parallel) and run each file's describe/it/test/expect hooks against the runtime.
  • Collect results, render output, and emit JUnit / GitHub Actions / --reporter=tap formats.
  • Support snapshot tests (toMatchSnapshot, toMatchInlineSnapshot), code coverage, watch mode, lifecycle hooks (beforeAll/beforeEach/...).
  • Handle bun test --filter, bun test --bail, bun test --rerun-each, bun test --coverage.

Directory layout

src/
├── cli/test_command.zig                 # the CLI entry, ~103 KB
├── cli/test/
│   ├── ChangedFilesFilter.zig           # --changed (only run files affected by changes)
│   ├── ParallelRunner.zig               # process-per-file controller
│   ├── Scanner.zig                      # discover test files
│   └── parallel/                        # per-worker helpers
└── bun.js/test/                         # `bun:test` JS API surface (expect, describe, it, ...)
    ├── Bun-test bindings...
    └── snapshot machinery

The actual expect implementation, matchers, snapshot serialiser, and timer/clock mocks all live under src/bun.js/test/ and are exposed as the bun:test hardcoded module.

Key abstractions

Type File Purpose
TestCommand src/cli/test_command.zig The argv parser and orchestrator. Decides how many workers to spawn and which files go to each.
Scanner src/cli/test/Scanner.zig Walks the working directory, applies test-file globs from bunfig.toml, and emits a list.
ParallelRunner src/cli/test/ParallelRunner.zig Manages the pool of child processes. Each child runs one file end-to-end.
ChangedFilesFilter src/cli/test/ChangedFilesFilter.zig Implements --changed: looks at the git working tree and limits scope.
Jest API surface src/bun.js/test/*.zig and src/js/bun/jest.ts The user-facing expect, describe, it, mock, spyOn.

How a test run works

sequenceDiagram
    participant CLI as src/cli/test_command.zig
    participant Scan as Scanner.zig
    participant Runner as ParallelRunner.zig
    participant Child as bun-test child
    participant VM as VirtualMachine.zig
    participant API as bun:test API

    CLI->>Scan: glob test files
    Scan-->>CLI: list of files
    CLI->>Runner: dispatch with concurrency
    par for each file
        Runner->>Child: spawn `bun --bun-test <file>`
        Child->>VM: bring up runtime
        VM->>API: load bun:test module
        VM->>VM: evaluate test file
        API-->>Runner: streaming results (TAP-ish)
        Child-->>Runner: exit code
    end
    Runner->>CLI: aggregate
    CLI-->>User: render summary

Parallelism is via OS processes, not threads. The default concurrency is os.availableParallelism() / 2. A failed worker doesn't tear down siblings.

The child process loads bun:test (a hardcoded module) which sets up globalThis.describe, globalThis.it, globalThis.expect, and friends. Test discovery within the file happens at evaluation time — it("name", fn) registers a test with the active describe block.

Snapshots

expect(x).toMatchSnapshot() writes serialised values to __snapshots__/<file>.snap. Inline snapshots (toMatchInlineSnapshot) rewrite the source file directly when --update-snapshots is passed. The serialiser is in src/bun.js/test/snapshot.zig plus pretty-printer hooks in src/bun.js/ConsoleObject.zig.

normalizeBunSnapshot(stdout) from test/harness.ts is the recommended helper: it strips ANSI, line numbers, and tempdir paths so snapshots stay stable across runs.

Coverage

bun test --coverage enables JSC's coverage profiler. Bun samples function/branch coverage and writes coverage/lcov.info (or text/JSON depending on flags). The integration is in src/cli/test_command.zig (look for enable_coverage) and the JSC binding is src/bun.js/bindings/BunCoverage.cpp.

Mocks and timers

mock(fn), mock.module(), spyOn(obj, "method"), vi.useFakeTimers() analogues are implemented in src/bun.js/test/. Fake timers reuse the runtime's Timer.zig machinery — they advance the timer treap manually instead of waiting for the OS.

Watch mode

bun test --watch plugs the file watcher (src/Watcher.zig) into the runner. On change, the runner re-runs only test files whose dependency graph touches the changed file. The graph is computed from the runtime's module loader cache.

CI integrations

Reporter Flag
TAP --reporter=tap
JUnit XML --reporter=junit --reporter-outfile=results.xml
GitHub Actions annotations auto-detected when GITHUB_ACTIONS=1
JSON streaming --reporter=json

The bundled CI helpers in package.json (bun run ci:errors, bun run ci:status) wrap BuildKite specifically.

bunfig.toml options

Test discovery and execution are configured under [test] in bunfig.toml. Common keys:

[test]
preload = ["./test/setup.ts"]
coverage = true
coverageThreshold = { line = 0.8, function = 0.8 }
coverageReporter = ["lcov", "text"]

Schema: src/bunfig.zig parses TOML and exposes the structured options into Arguments.zig.

Test conventions in this repo

The project's own tests under test/ follow rules documented in test/CLAUDE.md and CLAUDE.md:

  • Tests live next to the API they cover. A Bun.serve bug goes in test/js/bun/http/serve.test.ts.
  • Use tempDir from test/harness.ts, never fs.mkdtempSync.
  • Use port: 0 and let the OS pick a port.
  • Use bunExe() and bunEnv from test/harness.ts to spawn child processes — this picks up USE_SYSTEM_BUN=1 for regression tests.
  • expect(stdout).toBe(...) before expect(exitCode).toBe(0). Better failure output.
  • Verify a regression test fails on main with USE_SYSTEM_BUN=1 bun test <file> and passes on the fix branch with bun bd test <file>.

Integration points

  • Runtimebun test is bun run with bun:test preloaded and the file-discovery machinery added. See Runtime.
  • Module loader — Test file resolution uses the same Module resolution path.
  • Snapshot serialiser — Borrows from src/bun.js/ConsoleObject.zig's formatter.
  • Watcher — Watch mode uses src/Watcher.zig.

Entry points for modification

  • To add a CLI flag, edit src/cli/test_command.zig and src/cli/Arguments.zig.
  • To add a matcher (expect(...).toBeFoo()), look at src/bun.js/test/expect.zig or the matcher TS file under src/js/bun/.
  • To change snapshot serialisation, edit src/bun.js/test/snapshot.zig and the inspector path in ConsoleObject.zig.
  • To support a new reporter, add a printer under src/cli/test/ and wire it through ParallelRunner.zig.

Key source files

File Purpose
src/cli/test_command.zig CLI entry, runner glue.
src/cli/test/ParallelRunner.zig Parallel worker pool.
src/cli/test/Scanner.zig Test file discovery.
src/bun.js/test/ bun:test JS API and matchers.
test/harness.ts Test helpers (tempDir, bunExe, normalizeBunSnapshot).

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Test runner – Bun wiki | Factory