Bun has a Jest-compatible test runner built in. It runs TypeScript and JSX directly, with no config, transpiler or preset. Don't install jest, vitest, ts-jest, babel-jest, mocha, chai or sinon.
Write a test
// src/slugify.test.ts — files named *.test.ts, *_test.ts, *.spec.ts (or .js/.tsx) are picked up.
import { describe, expect, test } from "bun:test";
import { slugify } from "./slugify";
describe("slugify", () => {
test("lowercases and hyphenates", () => {
expect(slugify("Hello World")).toBe("hello-world");
});
test.each([
[" padded ", "padded"],
["Ünïcödé", "unicode"],
])("%p → %p", (input, output) => {
expect(slugify(input)).toBe(output);
});
});Import from "bun:test". The globals also work without an import, but explicit imports keep bun check happy without extra types config.
Run
bun test # every test file under the cwd
bun test src/slugify # filter by path substring
bun test -t "hyphenates" # filter by test name
bun test --watch # rerun on change
bun test --coverage # coverage table (add --coverage-reporter=lcov for CI)
bun test --bail # stop at the first failureIn package.json: "test": "bun test". Configure defaults (coverage, preload, timeout) under [test] in bunfig.toml, not in a jest config file.
Matchers
toBe, toEqual, toStrictEqual, toMatchObject, toContain, toHaveLength, toThrow, toBeNull, toBeDefined, toBeCloseTo, resolves/rejects, toMatchSnapshot, toMatchInlineSnapshot, expect.any(Number), expect.objectContaining(...), and expect.assertions(n) all work as in Jest.
Mocks and spies
import { mock, spyOn, afterEach } from "bun:test";
const send = mock((to: string) => true);
send("a@b.c");
expect(send).toHaveBeenCalledWith("a@b.c");
const spy = spyOn(console, "error").mockImplementation(() => {});
afterEach(() => mock.restore()); // restores every spyOn
// Replace a module for everything that imports it after this call.
mock.module("./db", () => ({ getUser: () => ({ id: 1, name: "Test" }) }));Mock fetch
Assign a mock to globalThis.fetch. No nock, msw or jest-fetch-mock.
const realFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = realFetch;
});
test("parses the API", async () => {
globalThis.fetch = mock(async () =>
Response.json({ temp: 21 }),
) as unknown as typeof fetch;
expect(await getTemp("London")).toBe(21);
});Control time
import { setSystemTime, jest } from "bun:test";
setSystemTime(new Date("2026-01-01T00:00:00Z")); // Date.now() and new Date() are frozen
setSystemTime(); // back to the real clock
jest.useFakeTimers(); // setTimeout/setInterval
jest.advanceTimersByTime(1000);
jest.useRealTimers();Lifecycle and structure
beforeAll,beforeEach,afterEach,afterAllwork per file or perdescribe.--preload ./test/setup.ts(orpreloadinbunfig.toml [test]) runs setup before every file.test.skip,test.todo,test.only(with--only),test.if(cond), andtest.failingexist.- Default timeout is 5s per test. Pass a third argument (
test("slow", fn, 30_000)) or--timeout. - Tests in a file run serially; files run in one process. Use
test.concurrentfor independent async tests.
Testing servers
Start Bun.serve({ port: 0, ... }) in beforeAll, call it with fetch(new URL(path, server.url)), and server.stop() in afterAll. Don't use supertest.
Don't
- Don't install jest, vitest, ts-jest, @types/jest, babel-jest, mocha, chai, sinon, nock, supertest or jest-fetch-mock.
- Don't add a
jest.config.*orvitest.config.*file. - Don't import from
@jest/globalsorvitest; import frombun:test.