Skip to main content
Define tests with a Jest-like API imported from the built-in bun:test module. All test functions (test, describe, expect, beforeAll, etc.) are also available as globals — no import required — for full Jest compatibility.

Basic usage

math.test.ts
import { expect, test } from "bun:test";

test("adds numbers", () => {
  expect(1 + 1).toBe(2);
});

Grouping tests

Use describe() to group related tests into a suite:
math.test.ts
import { expect, test, describe } from "bun:test";

describe("arithmetic", () => {
  test("addition", () => {
    expect(2 + 2).toBe(4);
  });

  test("multiplication", () => {
    expect(2 * 3).toBe(6);
  });
});

Async tests

Tests can be async. Bun awaits the returned promise before marking the test as complete.
import { expect, test } from "bun:test";

test("resolves a value", async () => {
  const result = await Promise.resolve(42);
  expect(result).toBe(42);
});
Alternatively, use the done callback. If you declare it as a parameter, you must call it or the test will time out.
test("done callback", done => {
  Promise.resolve(42).then(result => {
    expect(result).toBe(42);
    done();
  });
});

Timeouts

Pass a timeout in milliseconds as the third argument to test(). The default is 5000ms.
test("slow operation", async () => {
  const data = await slowOperation();
  expect(data).toBeDefined();
}, 500); // must complete in under 500ms

Test modifiers

test.skip

Skip a test without deleting it. Skipped tests are not executed.
test.skip("floating point precision", () => {
  expect(0.1 + 0.2).toEqual(0.3); // TODO: fix
});

test.todo

Mark a test as a planned but unimplemented case:
test.todo("implement retry logic");
Run bun test --todo to find any todo tests that are unexpectedly passing.

test.only

Run only the tests (or suites) marked with .only. Use bun test --only to enforce this.
test("test #1", () => {
  // skipped with --only
});

test.only("test #2", () => {
  // runs
});

describe.only("suite", () => {
  test("test #3", () => {
    // runs
  });
});

test.if / test.skipIf / test.todoIf

Run, skip, or mark a test as todo based on a runtime condition:
const isMacOS = process.platform === "darwin";

test.if(isMacOS)("runs only on macOS", () => { /* ... */ });

test.skipIf(isMacOS)("skipped on macOS", () => { /* ... */ });

test.todoIf(isMacOS)("not yet implemented on macOS", () => { /* ... */ });
These modifiers also work on describe blocks, affecting all tests within.

test.failing

Use test.failing to mark a test you know is currently broken. The test passes when it fails, and fails when it unexpectedly passes:
test.failing("known floating point issue", () => {
  expect(0.1 + 0.2).toBe(0.3); // fails due to float precision — expected
});

test.concurrent / test.serial

Control concurrency at the individual test level. See Test Runner for details.

Assertion counting

Use expect.assertions() to verify a specific number of assertions ran — essential for async code with conditional branches:
test("exactly two assertions", () => {
  expect.assertions(2);

  expect(1 + 1).toBe(2);
  expect("hello").toContain("ell");
});
Use expect.hasAssertions() to require at least one assertion:
test("async assertion runs", async () => {
  expect.hasAssertions();

  const data = await fetchData();
  expect(data).toBeDefined();
});

Retries and repeats

// Retry up to 3 times on failure
test("flaky test", async () => {
  const res = await fetch("https://api.example.com");
  expect(res.ok).toBe(true);
}, { retry: 3 });

// Run 21 times total (1 initial + 20 repeats) — fails if any iteration fails
test("stability check", () => {
  expect(Math.random()).toBeLessThan(1);
}, { repeats: 20 });
You cannot use both retry and repeats on the same test.

Parameterized tests

test.each

Run the same test with multiple sets of data:
test.each([
  [1, 2, 3],
  [3, 4, 7],
])("%i + %i should equal %i", (a, b, expected) => {
  expect(a + b).toBe(expected);
});
Pass objects to access values by name in the title using $key syntax:
test.each([
  { a: 1, b: 2, expected: 3 },
  { a: 4, b: 5, expected: 9 },
])("add($a, $b) = $expected", ({ a, b, expected }) => {
  expect(a + b).toBe(expected);
});

describe.each

Create a parameterized suite that runs all its tests for each case:
describe.each([
  [1, 2, 3],
  [3, 4, 7],
])("add(%i, %i)", (a, b, expected) => {
  test(`returns ${expected}`, () => {
    expect(a + b).toBe(expected);
  });

  test("result is greater than each input", () => {
    expect(a + b).toBeGreaterThan(a);
    expect(a + b).toBeGreaterThan(b);
  });
});

Format specifiers

SpecifierDescription
%ppretty-format
%sString
%dNumber
%iInteger
%fFloating point
%jJSON
%oObject
%#Index of the test case
%%Literal %

Type testing

Bun includes expectTypeOf for type-level assertions, compatible with Vitest. These are no-ops at runtime — run bunx tsc --noEmit to validate types.
import { expectTypeOf } from "bun:test";

expectTypeOf(123).toBeNumber();
expectTypeOf("hello").toBeString();
expectTypeOf([1, 2, 3]).items.toBeNumber();
expectTypeOf(Promise.resolve(42)).resolves.toBeNumber();

Matchers reference

Basic

MatcherDescription
.toBe()Strict equality (===)
.toEqual()Deep equality
.toStrictEqual()Deep equality with type checks
.toBeNull()Value is null
.toBeUndefined()Value is undefined
.toBeDefined()Value is not undefined
.toBeTruthy()Value is truthy
.toBeFalsy()Value is falsy
.toBeNaN()Value is NaN
.notNegates the matcher

Strings and arrays

MatcherDescription
.toContain()String or array contains a value
.toHaveLength()Array or string has a specific length
.toMatch()String matches regex or substring
.toContainEqual()Array contains an equal element
.stringContaining()Expect helper for string substring
.arrayContaining()Expect helper for array subset

Objects

MatcherDescription
.toMatchObject()Object matches a subset of properties
.toHaveProperty()Object has a specific property path
.objectContaining()Expect helper for object subset

Numbers

MatcherDescription
.toBeGreaterThan()Greater than a value
.toBeGreaterThanOrEqual()Greater than or equal to a value
.toBeLessThan()Less than a value
.toBeLessThanOrEqual()Less than or equal to a value
.toBeCloseTo()Approximately equal (floating pt)

Functions and errors

MatcherDescription
.toThrow()Function throws an error
.toBeInstanceOf()Value is an instance of a class
.resolvesPromise resolves to a value
.rejectsPromise rejects with an error

Mock functions

MatcherDescription
.toHaveBeenCalled()Mock was called at least once
.toHaveBeenCalledTimes(n)Mock was called exactly n times
.toHaveBeenCalledWith(...)Mock was called with specific arguments
.toHaveReturned()Mock returned without throwing
.toHaveReturnedWith(value)Mock returned a specific value

Snapshots

MatcherDescription
.toMatchSnapshot()Matches a stored snapshot
.toMatchInlineSnapshot()Matches an inline snapshot
.toThrowErrorMatchingSnapshot()Error message matches snapshot
.toThrowErrorMatchingInlineSnapshot()Error message matches inline snap

Best practices

// Good
test("returns 404 when user does not exist", () => { /* ... */ });

// Avoid
test("user test", () => { /* ... */ });
// Good
expect(users).toHaveLength(3);
expect(user.email).toContain("@");

// Avoid
expect(users.length === 3).toBe(true);
test("throws on invalid input", () => {
  expect(() => validateEmail("not-an-email")).toThrow("Invalid email");
});

test("rejects with network error", async () => {
  await expect(fetchUser("bad-id")).rejects.toThrow("User not found");
});

Build docs developers (and LLMs) love