Bun Test Runner
Bun's built-in test runner: bun test command, describe/it/expect assertions, mocking and spies, snapshot testing, lifecycle hooks, DOM testing with happy-dom, code coverage, and watch mode.
You are an expert in Bun's built-in test runner, covering test structure, assertions, mocking, DOM testing, coverage, and integration with existing Jest-style test suites. ## Key Points - **Importing from `@jest/globals` instead of `bun:test`**: While Bun has Jest compatibility, always import from `bun:test` for the best experience. Jest globals may not all be available. - **Not cleaning up mocks between tests**: Use `afterEach` to call `mockRestore()` or `mockReset()` on spies and mocks. Leaked mocks cause flaky tests. - **Writing tests that depend on execution order**: Each test should be independent. Do not rely on one test setting up state for another. - **Using `it.only` in committed code**: Focus modifiers (`it.only`, `describe.only`) should only be used during local debugging. They silently skip other tests in CI. - **Snapshot testing everything**: Snapshots are useful for complex output like rendered HTML or serialized data structures. Do not snapshot simple values -- use explicit assertions instead. - **Skipping `--frozen-lockfile` in CI test runs**: Install dependencies with `bun install --frozen-lockfile` before running tests in CI to ensure reproducibility. ## Quick Example ```bash # Update snapshots bun test --update-snapshots ``` ```bash bun add -d @happy-dom/global-registrator ```
skilldb get bun-skills/Bun Test RunnerFull skill: 411 linesBun Test Runner — Fast, Jest-Compatible Testing
You are an expert in Bun's built-in test runner, covering test structure, assertions, mocking, DOM testing, coverage, and integration with existing Jest-style test suites.
Overview
Bun includes a test runner that is largely compatible with Jest's API. It natively runs TypeScript and JSX test files without any configuration. Tests run significantly faster than Jest because there is no transpilation step and the runtime itself is faster. The test runner supports describe, it/test, expect, lifecycle hooks, mocks, spies, snapshots, and code coverage out of the box.
Running Tests
# Run all test files
bun test
# Run specific file
bun test src/utils.test.ts
# Run tests matching a pattern
bun test --filter "user"
# Watch mode -- re-run on file changes
bun test --watch
# Run with coverage
bun test --coverage
# Run with timeout (default is 5000ms)
bun test --timeout 10000
# Bail on first failure
bun test --bail
# Run tests in a specific directory
bun test tests/
Bun discovers test files by looking for files matching: *.test.{ts,tsx,js,jsx}, *_test.{ts,tsx,js,jsx}, *.spec.{ts,tsx,js,jsx}, and *_spec.{ts,tsx,js,jsx}. Files inside __tests__ directories are also picked up.
Test Structure
import { describe, it, test, expect, beforeAll, afterAll, beforeEach, afterEach } from "bun:test";
describe("Calculator", () => {
let calc: Calculator;
beforeEach(() => {
calc = new Calculator();
});
it("should add two numbers", () => {
expect(calc.add(2, 3)).toBe(5);
});
it("should handle negative numbers", () => {
expect(calc.add(-1, -2)).toBe(-3);
});
test("division by zero throws", () => {
expect(() => calc.divide(1, 0)).toThrow("Division by zero");
});
describe("advanced operations", () => {
it("should compute square root", () => {
expect(calc.sqrt(9)).toBe(3);
});
});
});
it and test are aliases -- use whichever you prefer.
Async Tests
it("fetches user data", async () => {
const user = await fetchUser(1);
expect(user.name).toBe("Alice");
});
it("rejects invalid input", async () => {
await expect(fetchUser(-1)).rejects.toThrow("Invalid ID");
});
Skipping and Focusing
it.skip("not implemented yet", () => {
// this test is skipped
});
it.todo("should handle edge case"); // placeholder, always skipped
it.only("run only this test", () => {
// only this test runs in the file
});
describe.skip("disabled suite", () => {
// all tests in this suite are skipped
});
Parameterized Tests
it.each([
[1, 2, 3],
[0, 0, 0],
[-1, 1, 0],
])("add(%d, %d) = %d", (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
it.each([
{ input: "hello", expected: 5 },
{ input: "", expected: 0 },
])("length of '$input' is $expected", ({ input, expected }) => {
expect(input.length).toBe(expected);
});
Expect Assertions
// Equality
expect(value).toBe(5); // strict equality (===)
expect(obj).toEqual({ a: 1, b: 2 }); // deep equality
expect(obj).toStrictEqual({ a: 1 }); // deep equality + type checking
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3);
expect(value).toBeLessThan(10);
expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // floating point comparison
// Strings
expect(str).toMatch(/pattern/);
expect(str).toContain("substring");
expect(str).toStartWith("hello");
expect(str).toEndWith("world");
// Arrays and iterables
expect(arr).toContain(item);
expect(arr).toHaveLength(3);
expect(arr).toEqual(expect.arrayContaining([1, 2]));
// Objects
expect(obj).toHaveProperty("key");
expect(obj).toHaveProperty("nested.key", "value");
expect(obj).toMatchObject({ subset: true });
expect(obj).toEqual(expect.objectContaining({ key: "val" }));
// Errors
expect(() => fn()).toThrow();
expect(() => fn()).toThrow("specific message");
expect(() => fn()).toThrow(TypeError);
// Negation
expect(value).not.toBe(5);
expect(arr).not.toContain(item);
Mocks and Spies
import { mock, spyOn, jest } from "bun:test";
// Create a mock function
const fn = mock((x: number) => x * 2);
fn(3);
fn(5);
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledTimes(2);
expect(fn).toHaveBeenCalledWith(3);
expect(fn).toHaveBeenLastCalledWith(5);
expect(fn.mock.results[0].value).toBe(6);
// Mock return values
const getter = mock(() => "default");
getter.mockReturnValue("mocked");
expect(getter()).toBe("mocked");
getter.mockReturnValueOnce("first").mockReturnValueOnce("second");
expect(getter()).toBe("first");
expect(getter()).toBe("second");
expect(getter()).toBe("mocked"); // back to default
// Mock implementation
const fetchMock = mock(async (url: string) => {
return { ok: true, status: 200 };
});
// Spy on object methods
const obj = {
greet(name: string) { return `Hello, ${name}`; }
};
const spy = spyOn(obj, "greet");
obj.greet("World");
expect(spy).toHaveBeenCalledWith("World");
// Spy and replace implementation
spyOn(obj, "greet").mockImplementation((name) => `Hi, ${name}`);
expect(obj.greet("Bun")).toBe("Hi, Bun");
// Reset mocks
fn.mockClear(); // clear call history
fn.mockReset(); // clear history + implementation
fn.mockRestore(); // restore original (for spies)
Mocking Modules
import { mock } from "bun:test";
// Mock an entire module
mock.module("./database", () => ({
query: mock(() => [{ id: 1, name: "Test" }]),
connect: mock(() => Promise.resolve()),
}));
// Now imports of "./database" return the mocked version
import { query } from "./database";
const results = query("SELECT * FROM users");
expect(results).toEqual([{ id: 1, name: "Test" }]);
Timer Mocks
import { jest } from "bun:test";
it("handles setTimeout", () => {
jest.useFakeTimers();
const fn = mock();
setTimeout(fn, 1000);
expect(fn).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
expect(fn).toHaveBeenCalled();
jest.useRealTimers();
});
Snapshot Testing
it("renders correctly", () => {
const output = renderComponent({ name: "Bun" });
expect(output).toMatchSnapshot();
});
// Inline snapshots
it("formats output", () => {
const result = format({ a: 1, b: 2 });
expect(result).toMatchInlineSnapshot(`"a=1, b=2"`);
});
# Update snapshots
bun test --update-snapshots
Snapshot files are stored in __snapshots__/ directories next to the test file, in the same format as Jest.
Lifecycle Hooks
import { describe, it, beforeAll, afterAll, beforeEach, afterEach } from "bun:test";
beforeAll(async () => {
// Runs once before all tests in the file
await setupDatabase();
});
afterAll(async () => {
// Runs once after all tests in the file
await teardownDatabase();
});
beforeEach(() => {
// Runs before each test
resetState();
});
afterEach(() => {
// Runs after each test
cleanupMocks();
});
describe("nested suite", () => {
beforeEach(() => {
// This runs after the outer beforeEach
});
it("has access to both setups", () => {
// ...
});
});
DOM Testing with happy-dom
Bun does not include a DOM implementation, but you can use happy-dom for DOM testing:
bun add -d @happy-dom/global-registrator
# bunfig.toml
[test]
preload = ["./test-setup.ts"]
// test-setup.ts
import { GlobalRegistrator } from "@happy-dom/global-registrator";
GlobalRegistrator.register();
// dom.test.ts
import { describe, it, expect } from "bun:test";
it("creates DOM elements", () => {
const div = document.createElement("div");
div.textContent = "Hello";
div.classList.add("greeting");
expect(div.textContent).toBe("Hello");
expect(div.className).toBe("greeting");
});
it("queries the DOM", () => {
document.body.innerHTML = `
<ul>
<li class="item">First</li>
<li class="item">Second</li>
</ul>
`;
const items = document.querySelectorAll(".item");
expect(items.length).toBe(2);
expect(items[0].textContent).toBe("First");
});
Code Coverage
# Run with coverage report
bun test --coverage
# Coverage output includes line, function, and branch coverage
# Output goes to stdout by default
Configure coverage in bunfig.toml:
[test]
coverage = true
coverageReporter = ["text", "lcov"]
coverageDir = "./coverage"
# Threshold enforcement
coverageThreshold = { line = 80, function = 80, statement = 80 }
The lcov reporter produces coverage/lcov.info for integration with coverage visualization tools like Codecov and Coveralls.
Watch Mode
# Re-run tests on file changes
bun test --watch
# Watch mode re-runs only the tests affected by changed files
# Press Enter to re-run all tests
# Press q to quit
Anti-Patterns
- Importing from
@jest/globalsinstead ofbun:test: While Bun has Jest compatibility, always import frombun:testfor the best experience. Jest globals may not all be available. - Not cleaning up mocks between tests: Use
afterEachto callmockRestore()ormockReset()on spies and mocks. Leaked mocks cause flaky tests. - Writing tests that depend on execution order: Each test should be independent. Do not rely on one test setting up state for another.
- Using
it.onlyin committed code: Focus modifiers (it.only,describe.only) should only be used during local debugging. They silently skip other tests in CI. - Snapshot testing everything: Snapshots are useful for complex output like rendered HTML or serialized data structures. Do not snapshot simple values -- use explicit assertions instead.
- Skipping
--frozen-lockfilein CI test runs: Install dependencies withbun install --frozen-lockfilebefore running tests in CI to ensure reproducibility.
Install this skill directly: skilldb add bun-skills
Related Skills
Bun Bundler
Bun's built-in bundler: Bun.build() API, entry points, output formats (esm, cjs, iife), plugins, loaders, tree shaking, code splitting, CSS bundling, HTML entries, and compile-time macros.
Bun Fundamentals
Bun runtime overview: all-in-one JavaScript runtime, bundler, test runner, and package manager. Installation, project initialization, Node.js compatibility, performance characteristics, and guidance on when to choose Bun vs Node.
Bun HTTP Server
Building HTTP servers with Bun: Bun.serve() API, routing patterns, WebSocket support, streaming responses, static file serving, TLS configuration, hot reloading, and integration with frameworks like Hono and Elysia.
Bun Node.js Migration
Migrating from Node.js to Bun: compatibility checklist, node:* module imports, native addon handling, environment variable differences, Docker setup, CI/CD pipeline changes, and common migration pitfalls.
Bun Package Manager
Bun as a package manager: bun install, bun add, bun remove, the binary lockfile (bun.lockb), workspace support, overrides, patching, publishing packages, global cache, and comparison to npm, pnpm, and yarn.
Bun Production Patterns
Production patterns for Bun: Docker deployments, TypeScript configuration, shell scripting with Bun.$, monorepo setup, database access patterns, and deployment to Fly.io and Railway.