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:
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);
});