The Test Command (bun test) is the CLI entry point for running Bun's Jest-compatible test framework. This document covers the command-line interface, argument parsing, test discovery, filtering, and integration with the test runner. For information about the test runner architecture and test execution internals, see 6.1. For CLI architecture and command routing, see 7.1.
The bun test command is invoked through the CLI and accepts various arguments to control test execution:
Basic Usage Examples:
| Command | Description |
|---|---|
bun test | Runs all test files in the current directory |
bun test path/to/file.test.ts | Runs a specific test file |
bun test path/to/dir | Runs all test files in a directory |
bun test --watch | Runs tests in watch mode |
bun test --bail | Stops after first failure |
Sources: package.json29-97 test/cli/test/bun-test.test.ts1-600
CLI Entry and Test Runner Initialization
Command Flow:
Command.ContextCommand.TestOptions structure is populated with CLI flagsTestRunner is initialized src/bun.js/test/jest.zig64-96:
files: File.List - Multi-array list of test filesindex: File.Map - Hash map from path hash to file IDonly: bool - Whether to run only .only testsbail: u32 - Number of failures before stoppingfilter_regex: ?*RegularExpression - Pattern for filtering testsCommandLineReporter is set up src/cli/test_command.zig569-593printTestLine() src/cli/test_command.zig599-719Sources: src/cli/test_command.zig569-593 src/bun.js/test/jest.zig64-96
Test discovery is handled by the test runner and CLI coordination:
File Registration and Tracking
Discovery Rules:
| Input Type | Behavior |
|---|---|
| No arguments | Discovers all *.test.* files in current directory recursively |
| Absolute file path | Runs that specific file if it exists |
| Relative file path | Resolves from current directory |
| Directory path | Discovers all test files in that directory |
| Multiple arguments | Processes each argument independently |
| Non-existent file | Returns exit code 1 test/cli/test/bun-test.test.ts8-17 |
File Tracking Data Structures:
The TestRunner src/bun.js/test/jest.zig64-172 maintains:
files: File.List = .{} // MultiArrayList of File structs
index: File.Map = File.Map{} // HashMap: u32 hash -> File.ID
Each File contains src/bun.js/test/jest.zig164-171:
source: logger.Source // File path and contents
log: logger.Log // Error/warning accumulator
The getOrPutFile() method src/bun.js/test/jest.zig153-162 uses file path hashing to deduplicate files and assign stable IDs. Note that the hash collision issue mentioned in the code comment means the same hash could theoretically map to different files.
Sources: src/bun.js/test/jest.zig64-172 src/bun.js/test/jest.zig153-162 test/cli/test/bun-test.test.ts8-17
| Option | Type | Description |
|---|---|---|
--test-name-pattern | RegularExpression | Filter tests by name pattern |
--only | boolean | Only run tests marked with .only |
--todo | boolean | Run tests marked with .todo |
| Files/directories | []const u8 | Paths to test files or directories |
Filter Implementation:
The TestRunner stores a filter_regex field:
filter_regex: ?*RegularExpression
This regex is checked in hasTestFilter() and applied during test enumeration.
Sources: src/bun.js/test/jest.zig92-95 src/bun.js/test/jest.zig130-132
| Option | Type | Description | Default |
|---|---|---|---|
--bail | u32 | Stop after N failures | 0 (disabled) |
--timeout | u32 | Default timeout in ms | 5000 |
--concurrent | boolean | Run tests concurrently | false |
--concurrent-test-glob | [][]const u8 | Glob patterns for concurrent tests | null |
--rerun-each | u32 | Rerun each test N times | 1 |
--randomize | boolean | Randomize test order | false |
Bail Implementation:
The bail field src/bun.js/test/jest.zig74 stops test execution after N failures:
When --bail=N is passed, the test runner tracks failures in summary.fail src/bun.js/test/jest.zig121 and terminates early if the threshold is reached. The output includes "Bailed out after N failures" in the summary.
Sources: src/bun.js/test/jest.zig74 src/bun.js/test/jest.zig117-128 test/cli/test/bun-test.test.ts298-359
| Option | Type | Description |
|---|---|---|
--reporter | enum | Output format (default, dots, junit) |
--junit | string | Path to JUnit XML report file |
--only-failures | boolean | Only show failing tests |
Reporter Configuration:
The CommandLineReporter contains a nested structure:
Sources: src/cli/test_command.zig569-585
The main reporter handles formatted output to stderr:
Key Fields:
jest: TestRunner - Reference to test runnercurrent_file: CurrentFile - Tracks the current file being reportedfailures_to_repeat_buf: ArrayListUnmanaged(u8) - Buffers failures for summaryreporters: struct - Configuration for reporter modesCurrentFile Tracking:
To avoid printing filenames multiple times, the CurrentFile struct defers printing:
When a test result needs to be printed, it calls printIfNeeded() which prints the filename only once.
Sources: src/cli/test_command.zig1-62 src/cli/test_command.zig569-593
The JunitReporter src/cli/test_command.zig73-171 generates XML output compatible with CI systems:
Structure:
XML Generation Flow:
Metrics Tracking:
The reporter uses a backpatching technique: it writes <testsuite> tags with placeholder positions, executes tests while accumulating metrics, then inserts the final metrics values at the stored offsets src/cli/test_command.zig348-360
CI Integration:
Properties are generated from environment variables src/cli/test_command.zig173-257:
GITHUB_RUN_ID, GITHUB_SERVER_URL, GITHUB_REPOSITORY → CI job URLGITHUB_SHA, CI_COMMIT_SHA, GIT_SHA → commit hashSources: src/cli/test_command.zig73-567 src/cli/test_command.zig266-329 src/cli/test_command.zig372-518
Test Lifecycle and Timeout Management
Active Test Tracking:
The TestRunner src/bun.js/test/jest.zig64-172 tracks the currently executing test through:
bun_test_root: bun_test.BunTestRoot - Root test context holding active filegetActiveTimeout() src/bun.js/test/jest.zig100-106 - Returns the timer.next timespec if activeremoveActiveTimeout() src/bun.js/test/jest.zig108-114 - Removes timer from VM when test completesTimeout Resolution:
The timeout precedence is:
test() function)default_timeout_override src/bun.js/test/jest.zig88 (set via setDefaultTimeout())default_timeout_ms src/bun.js/test/jest.zig85 (from CLI or defaults to 5000ms)Sources: src/bun.js/test/jest.zig64-172 src/bun.js/test/jest.zig85-114
Exit Code Logic:
| Condition | Exit Code |
|---|---|
| All tests pass | 0 |
| Any test fails | 1 |
| Parse error | 1 |
| Label filter matched no tests | 0 or 1 (depends on context) |
Summary Output Format:
X pass
Y fail
Z skip
A todo
B expectations
Ran C tests across D files. [E ms]
If bail was triggered:
Bailed out after N failures
Sources: src/bun.js/test/jest.zig116-128 src/cli/test_command.zig599-714
While not extensively documented in the provided files, the test command supports --watch mode:
This mode monitors file system changes and re-runs affected tests. The implementation uses Bun's file system watching capabilities.
Sources: package.json32
Tests can run concurrently based on configuration:
Concurrency Decision Tree
Implementation:
The shouldFileRunConcurrently() method src/bun.js/test/jest.zig134-151 determines whether a file should run concurrently:
CLI Integration:
The --concurrent flag sets the global concurrent field, while --concurrent-test-glob provides patterns to selectively enable concurrency for specific files. This allows fine-grained control over which tests run in parallel.
Sources: src/bun.js/test/jest.zig134-151
The TestRunner maintains timeout configuration:
Timeout Precedence:
test() function)default_timeout_override (set by setDefaultTimeout())default_timeout_ms (from command-line or defaults to 5000ms)Tests can override the default timeout:
The jsSetDefaultTimeout() function in Jest implements this by setting test_runner.default_timeout_override.
Sources: src/bun.js/test/jest.zig85-89 src/bun.js/test/jest.zig289-302
Errors that occur outside of tests are reported as "Unhandled error between tests":
Error Attribution Logic
The on_unhandled_rejection.onUnhandledRejection() function src/bun.js/test/jest.zig310-333 determines error attribution:
BunTest context exists, error is reported via VM's default handlercurrent_state_data is set to .start to mark it as "unhandled error between tests"onUncaughtException() and test execution continuesThis prevents unhandled promise rejections from crashing the test runner while still attributing errors correctly.
Sources: src/bun.js/test/jest.zig309-334
Test results are formatted with status symbols:
| Status | Symbol (with color) | Symbol (no color) |
|---|---|---|
| Pass | ✓ (green) | (pass) |
| Fail | ✗ (red) | (fail) |
| Skip | » (yellow) | (skip) |
| Todo | ✎ (magenta) | (todo) |
| Pending | … (dim) | (pending) |
Sources: src/cli/test_command.zig42-68
The filter_regex field src/bun.js/test/jest.zig93 enables filtering tests by name:
Usage:
The hasTestFilter() method src/bun.js/test/jest.zig130-132 checks if filtering is active, and the regex is applied during test enumeration to skip non-matching tests.
formatLabel()The formatLabel() function src/bun.js/test/jest.zig357-468 generates test labels by injecting parameters using printf-style formatting:
Supported Format Specifiers:
| Specifier | Type | Description |
|---|---|---|
%s | string | String interpolation |
%i | integer | Integer value |
%d | number | Number value |
%f | number | Float value |
%j, %o | any | JSON serialization via jsonStringifyFast() |
%p | any | Pretty-printed value via ConsoleObject.Formatter |
%# | - | Test index |
%% | - | Literal % |
$identifier | any | Property path access (e.g., $user.name) |
Format Label Flow:
Example Usage:
The function handles special cases src/bun.js/test/jest.zig389-404:
$user.name)toString() to avoid adding quotes (Jest compatibility)ConsoleObject.Formatter for pretty printingSources: src/bun.js/test/jest.zig93 src/bun.js/test/jest.zig130-132 src/bun.js/test/jest.zig357-468
The TestRunner.getOrPutFile() method handles file path tracking:
This ensures each file is tracked only once using a hash-based index.
Sources: src/bun.js/test/jest.zig153-162
Refresh this wiki