The Complete Guide to Bun's Built-in Test Runner
If you're already using Bun as your runtime or package manager, you might not realize it ships with a full-featured test runner built in. No extra dependencies, no configuration files, no transpilation step. Just bun test and your TypeScript tests run natively at speeds that make Jest feel like it's stuck in traffic.

Why Use Bun's Test Runner?
The pitch is simple: Bun's test runner runs TypeScript natively (no ts-jest, no babel, no esbuild plugin), uses a Jest-compatible API (so most of your existing tests work unchanged), and is dramatically faster — typically 10-100x faster than Jest for TypeScript projects.
It's not just faster startup. Bun's test runner:
- Runs TypeScript and JSX natively — No transpilation step, no configuration
- Jest-compatible API —
describe,test,expect,beforeEach,afterAll, etc. - Built-in mocking —
mock(),spyOn(), module mocking - Snapshot testing — Same
.toMatchSnapshot()you know from Jest - Code coverage —
--coverageflag with lcov output - Watch mode —
--watchfor automatic re-runs on file changes - Lifecycle hooks — Setup and teardown at every level
- Test filtering — Run specific tests by name pattern
Getting Started
If you have Bun installed, you already have the test runner. Create a test file:
// math.test.ts
import { expect, test, describe } from "bun:test";
function add(a: number, b: number): number {
return a + b;
}
describe("add", () => {
test("adds two positive numbers", () => {
expect(add(1, 2)).toBe(3);
});
test("handles negative numbers", () => {
expect(add(-1, 1)).toBe(0);
});
test("handles zero", () => {
expect(add(0, 0)).toBe(0);
});
});
Run it:
bun test
Bun automatically finds test files matching these patterns:
*.test.ts,*.test.tsx,*.test.js,*.test.jsx*.spec.ts,*.spec.tsx,*.spec.js,*.spec.jsx- Files inside
__tests__/directories
Matchers
Bun implements the full Jest expect() API. Here are the most commonly used matchers:
import { expect, test } from "bun:test";
test("matchers showcase", () => {
// Equality
expect(1 + 1).toBe(2);
expect({ a: 1 }).toEqual({ a: 1 });
expect({ a: 1, b: 2 }).toMatchObject({ a: 1 });
// Truthiness
expect(true).toBeTruthy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect("hello").toBeDefined();
// Numbers
expect(0.1 + 0.2).toBeCloseTo(0.3);
expect(10).toBeGreaterThan(5);
expect(3).toBeLessThanOrEqual(3);
// Strings
expect("hello world").toContain("world");
expect("hello world").toMatch(/hello/);
// Arrays
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
// Errors
expect(() => { throw new Error("boom"); }).toThrow("boom");
expect(() => { throw new Error("boom"); }).toThrow(/boom/);
});
Asymmetric Matchers
These are useful when you don't care about exact values in part of a comparison:
test("asymmetric matchers", () => {
const user = { id: 1, name: "Alice", createdAt: new Date() };
expect(user).toEqual({
id: expect.any(Number),
name: expect.stringContaining("Ali"),
createdAt: expect.any(Date),
});
expect([1, "hello", true]).toEqual(
expect.arrayContaining([1, true])
);
});
Lifecycle Hooks
Control setup and teardown with the same hooks as Jest:
import { describe, test, expect, beforeAll, beforeEach, afterEach, afterAll } from "bun:test";
describe("database tests", () => {
let db: Database;
beforeAll(async () => {
db = await Database.connect();
await db.migrate();
});
beforeEach(async () => {
await db.beginTransaction();
});
afterEach(async () => {
await db.rollback();
});
afterAll(async () => {
await db.disconnect();
});
test("creates a user", async () => {
const user = await db.users.create({ name: "Alice" });
expect(user.id).toBeDefined();
});
});
Hooks can be async and Bun will await them. They run in the expected order: beforeAll once before the suite, beforeEach before each test, afterEach after each test, afterAll once after all tests.
Mocking
Bun provides built-in mocking without needing external libraries.
Function Mocks
import { test, expect, mock } from "bun:test";
test("mock functions", () => {
const fn = mock((x: number) => x * 2);
fn(1);
fn(2);
fn(3);
expect(fn).toHaveBeenCalledTimes(3);
expect(fn).toHaveBeenCalledWith(2);
expect(fn.mock.results[0].value).toBe(2);
});
Spying on Methods
import { test, expect, spyOn } from "bun:test";
test("spy on console.log", () => {
const spy = spyOn(console, "log");
console.log("hello");
expect(spy).toHaveBeenCalledWith("hello");
spy.mockRestore();
});
Module Mocking
Mock entire modules with mock.module():
import { test, expect, mock } from "bun:test";
mock.module("./api-client", () => ({
fetchUser: mock(() => Promise.resolve({ id: 1, name: "Alice" })),
}));
// Now any import of ./api-client gets the mock
import { fetchUser } from "./api-client";
test("uses mocked API client", async () => {
const user = await fetchUser();
expect(user.name).toBe("Alice");
});
Snapshot Testing
Snapshot testing works the same as in Jest:
import { test, expect } from "bun:test";
test("renders user card", () => {
const html = renderUserCard({ name: "Alice", role: "admin" });
expect(html).toMatchSnapshot();
});
On first run, Bun creates a __snapshots__ directory with .snap files. On subsequent runs, it compares against the saved snapshot.
Update snapshots when your output changes intentionally:
bun test --update-snapshots
Async Testing
Bun handles async tests naturally — return a promise or use async/await:
import { test, expect } from "bun:test";
test("async with await", async () => {
const response = await fetch("https://api.example.com/health");
expect(response.status).toBe(200);
});
test("async with promises", () => {
return fetch("https://api.example.com/health")
.then((res) => expect(res.status).toBe(200));
});
Timeouts
Set per-test timeouts (in milliseconds):
test("slow operation", async () => {
const result = await slowOperation();
expect(result).toBeDefined();
}, 10000); // 10 second timeout
Code Coverage
Generate coverage reports with the --coverage flag:
bun test --coverage
This outputs a summary to the terminal. For CI integration, generate an lcov report:
bun test --coverage --coverage-reporter=lcov
The lcov file is written to coverage/lcov.info, compatible with tools like Codecov, Coveralls, and SonarQube.
Watch Mode
Re-run tests automatically when files change:
bun test --watch
This watches your source and test files for changes and re-runs only the affected tests. It's fast enough that you can keep it running in a terminal while you code.
Filtering Tests
Run specific tests by pattern:
# Run tests matching a name pattern
bun test --test-name-pattern "user"
# Run a specific test file
bun test src/utils/format.test.ts
# Run tests in a specific directory
bun test src/utils/
Skip or focus tests in code:
test.skip("not ready yet", () => {
// This test won't run
});
test.only("just this one", () => {
// Only this test runs (in this file)
});
test.todo("write me later");
Configuration
Bun's test runner is configured through bunfig.toml (not a separate config file):
[test]
# Custom test file patterns
root = "./src"
# Preload scripts (setup files that run before tests)
preload = ["./test/setup.ts"]
# Coverage settings
coverage = true
coverageThreshold = { line = 80, function = 80, statement = 80 }
# Timeout in milliseconds
timeout = 5000
# Rerun tests on change
watch = false
Preload Scripts
Setup files that run before any test file — useful for global configuration:
// test/setup.ts
import { afterAll } from "bun:test";
// Global test database connection
globalThis.testDb = await createTestDatabase();
afterAll(async () => {
await globalThis.testDb.destroy();
});
Migrating from Jest
Most Jest tests work with Bun's test runner with minimal changes:
- Change imports from
@jest/globalsor globaljesttobun:test:
// Before (Jest)
import { describe, test, expect, jest } from "@jest/globals";
const mockFn = jest.fn();
// After (Bun)
import { describe, test, expect, mock } from "bun:test";
const mockFn = mock();
- Update module mocking —
jest.mock()becomesmock.module():
// Before (Jest)
jest.mock("./api-client");
// After (Bun)
mock.module("./api-client", () => ({ ... }));
Remove transpilation config — Delete
ts-jestconfig,babel.config.jstest presets, and any test-specifictsconfig.json. Bun handles TypeScript natively.Update scripts in
package.json:
{
"scripts": {
"test": "bun test",
"test:watch": "bun test --watch",
"test:coverage": "bun test --coverage"
}
}
What Doesn't Work Yet
A few Jest features aren't available in Bun's test runner (as of early 2026):
jest.useFakeTimers()— Not yet supported. Usemock()to mocksetTimeout/setIntervalmanually- Custom reporters — Bun outputs its own format; custom Jest reporters won't work
jest.requireActual()— Use dynamic imports instead- Concurrent test suites — Tests within a
describerun serially; parallel execution is file-level
Migrating from Vitest
Vitest migrations are even simpler since Vitest's API is already Jest-compatible:
// Before (Vitest)
import { describe, test, expect, vi } from "vitest";
const spy = vi.fn();
// After (Bun)
import { describe, test, expect, mock } from "bun:test";
const spy = mock();
The main difference is replacing vi with mock/spyOn from bun:test. Most Vitest tests work with a simple find-and-replace.
Performance Comparison
On a typical TypeScript project with 200 tests:
| Runner | Cold Start | Warm Start | Notes |
|---|---|---|---|
| Jest + ts-jest | ~8-12s | ~4-6s | Transpiles every file |
| Jest + SWC | ~3-5s | ~2-3s | Faster transpiler |
| Vitest | ~2-4s | ~1-2s | Uses Vite's transform |
| Bun test | ~0.2-0.5s | ~0.1-0.3s | No transpilation step |
The speed difference is most dramatic with TypeScript projects because Bun doesn't need a separate transpilation step — it understands TypeScript natively.
Best Practices
Organize tests next to source files — utils.ts alongside utils.test.ts makes imports simpler and keeps related code together.
Use describe blocks for grouping — Group related tests by feature or function. This makes test output readable and lets you share setup logic via beforeEach.
Prefer toEqual for objects — toBe uses Object.is() (reference equality), which fails for object comparisons. Use toEqual for deep equality checks.
Mock at boundaries — Mock external services, file system calls, and network requests. Don't mock internal functions unless you're testing interaction patterns.
Keep tests fast — Bun's test runner is fast, but slow tests are still slow. Mock expensive operations, use in-memory databases for unit tests, and save real database tests for integration suites.
Conclusion
Bun's test runner eliminates the most frustrating parts of JavaScript testing: slow startup, transpilation configuration, and dependency installation. If you're already using Bun (or considering it), there's very little reason to use a separate test runner. The Jest-compatible API means migration is straightforward, and the speed improvement is immediately noticeable. For new projects, bun test is one of the strongest arguments for adopting Bun as your primary JavaScript runtime.