Skip to main content
Bun’s test runner provides a Jest-compatible API for writing tests. If you’re familiar with Jest, Vitest, or other test frameworks, you’ll feel right at home.

Basic test structure

Tests are defined using test() or it() functions:
import { test, expect } from "bun:test";

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

// "it" is an alias for "test"
it("subtraction works", () => {
  expect(5 - 3).toBe(2);
});

Grouping tests with describe

Use describe() to group related tests:
import { describe, test, expect } from "bun:test";

describe("Math operations", () => {
  test("addition", () => {
    expect(1 + 1).toBe(2);
  });

  test("subtraction", () => {
    expect(5 - 3).toBe(2);
  });

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

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

Async tests

Tests can be async. Bun will wait for the promise to resolve:
import { test, expect } from "bun:test";

test("async test", async () => {
  const response = await fetch("https://example.com");
  expect(response.status).toBe(200);
});

test("promise test", () => {
  return fetch("https://example.com").then(response => {
    expect(response.status).toBe(200);
  });
});

Test timeout

By default, tests timeout after 5 seconds. You can customize this:
import { test, expect } from "bun:test";

// Set timeout for a specific test
test("slow operation", async () => {
  await slowOperation();
}, 10000); // 10 second timeout

// Set timeout globally in bunfig.toml
// [test]
// timeout = 10000

Skipping tests

Skip tests with .skip() or test.skip():
import { test, expect } from "bun:test";

// Skip a single test
test.skip("this test will be skipped", () => {
  expect(true).toBe(false);
});

// Alternative syntax
test("also skipped", () => {
  expect(true).toBe(false);
}).skip();

// Skip a describe block
describe.skip("skipped suite", () => {
  test("will not run", () => {
    // ...
  });
});

Only running specific tests

Run only specific tests with .only():
import { test, expect } from "bun:test";

test.only("only this test will run", () => {
  expect(true).toBe(true);
});

test("this test will be skipped", () => {
  expect(true).toBe(true);
});
You can also use the --only flag to run only tests marked with .only() across your entire test suite:
$ bun test --only

Todo tests

Mark tests as todo when you’re planning to implement them:
import { test } from "bun:test";

test.todo("implement this feature");

test.todo("test this edge case", () => {
  // Test implementation
});

Concurrent tests

Run tests concurrently to speed up test execution:
import { test, expect } from "bun:test";

test.concurrent("test 1", async () => {
  await fetch("https://api1.example.com");
});

test.concurrent("test 2", async () => {
  await fetch("https://api2.example.com");
});

// Run all tests in a describe block concurrently
describe.concurrent("API tests", () => {
  test("endpoint 1", async () => {
    // ...
  });
  
  test("endpoint 2", async () => {
    // ...
  });
});

Expectations

Bun provides a Jest-compatible expect() API:
import { test, expect } from "bun:test";

test("matchers", () => {
  // Equality
  expect(2 + 2).toBe(4);
  expect([1, 2]).toEqual([1, 2]);
  
  // Truthiness
  expect(true).toBeTruthy();
  expect(false).toBeFalsy();
  expect(null).toBeNull();
  expect(undefined).toBeUndefined();
  
  // Numbers
  expect(10).toBeGreaterThan(5);
  expect(5).toBeLessThan(10);
  expect(0.1 + 0.2).toBeCloseTo(0.3);
  
  // Strings
  expect("hello world").toMatch(/world/);
  expect("hello").toContain("ell");
  
  // Arrays
  expect([1, 2, 3]).toContain(2);
  expect([1, 2, 3]).toHaveLength(3);
  
  // Objects
  expect({ a: 1, b: 2 }).toHaveProperty("a");
  expect({ a: 1, b: 2 }).toMatchObject({ a: 1 });
  
  // Exceptions
  expect(() => {
    throw new Error("fail");
  }).toThrow("fail");
  
  // Async
  await expect(Promise.resolve(42)).resolves.toBe(42);
  await expect(Promise.reject("error")).rejects.toBe("error");
});

Negation

Negate any matcher with .not:
import { test, expect } from "bun:test";

test("negation", () => {
  expect(1).not.toBe(2);
  expect([1, 2]).not.toContain(3);
});

Custom matchers

Extend expect with custom matchers:
import { expect } from "bun:test";

expect.extend({
  toBeWithinRange(received: number, min: number, max: number) {
    const pass = received >= min && received <= max;
    return {
      pass,
      message: () =>
        pass
          ? `expected ${received} not to be within range ${min} - ${max}`
          : `expected ${received} to be within range ${min} - ${max}`,
    };
  },
});

test("custom matcher", () => {
  expect(5).toBeWithinRange(1, 10);
});

Test context

You can optionally pass a test context object with the done callback:
import { test, expect } from "bun:test";

test("using done callback", (done) => {
  setTimeout(() => {
    expect(true).toBe(true);
    done();
  }, 100);
});
Prefer using async/await over done callbacks when possible, as it’s more readable and easier to debug.

Parameterized tests

Run the same test with different inputs:
import { test, expect } from "bun:test";

test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
])("%i + %i should equal %i", (a, b, expected) => {
  expect(a + b).toBe(expected);
});

// With objects
test.each([
  { a: 1, b: 1, expected: 2 },
  { a: 1, b: 2, expected: 3 },
  { a: 2, b: 1, expected: 3 },
])("$a + $b = $expected", ({ a, b, expected }) => {
  expect(a + b).toBe(expected);
});

Build docs developers (and LLMs) love