使用从内置模块 bun:test 导入的类似 Jest 的 API 来定义测试。长期来看,Bun 目标是实现完全的 Jest 兼容性;目前支持的 expect 匹配器集合有限。
基本用法
定义一个简单的测试:

math.test.ts
import { expect, test } from "bun:test";
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
测试分组
使用 describe 将测试分组到测试套件中。

math.test.ts
import { expect, test, describe } from "bun:test";
describe("算术", () => {
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
test("2 * 2", () => {
expect(2 * 2).toBe(4);
});
});
异步测试
测试可以是异步的。

math.test.ts
import { expect, test } from "bun:test";
test("2 * 2", async () => {
const result = await Promise.resolve(2 * 2);
expect(result).toEqual(4);
});
或者使用 done 回调来标识测试完成。如果在测试定义中包含了 done 回调参数,必须调用它,否则测试将挂起。

math.test.ts
import { expect, test } from "bun:test";
test("2 * 2", done => {
Promise.resolve(2 * 2).then(result => {
expect(result).toEqual(4);
done();
});
});
可通过给 test 传入第三个参数数字来指定单个测试的超时时间(毫秒)。

math.test.ts
import { test } from "bun:test";
test("wat", async () => {
const data = await slowOperation();
expect(data).toBe(42);
}, 500); // 测试必须在 <500ms 内完成
在 bun:test 中,测试超时会抛出不可捕获的异常,强制测试停止并判定失败。与此同时,测试中生成的任何子进程也会被杀死,以避免后台留下僵尸进程。
如果未被此超时选项或 jest.setDefaultTimeout() 覆盖,每个测试的默认超时是 5000ms(5 秒)。
重试和重复执行
test.retry
使用 retry 选项可在测试失败时自动重试。只要在指定的尝试次数内成功,测试即通过。适合不稳定、偶尔失败的测试。

example.test.ts
import { test } from "bun:test";
test(
"不稳定的网络请求",
async () => {
const response = await fetch("https://example.com/api");
expect(response.ok).toBe(true);
},
{ retry: 3 }, // 失败时最多重试3次
);
test.repeats
使用 repeats 选项可无视测试结果,重复运行测试多次。只要有任一轮失败,即判定测试失败。适合检测不稳定测试或压力测试。注意,repeats: N 实际运行次数为 N+1(1次初始 + N次重复)。

example.test.ts
import { test } from "bun:test";
test(
"确保测试稳定",
() => {
expect(Math.random()).toBeLessThan(1);
},
{ repeats: 20 }, // 总计运行21次(1次初始 + 20次重复)
);
同一个测试不能同时使用 retry 和 repeats。
🧟 僵尸进程清理器
当测试超时且通过 Bun.spawn、Bun.spawnSync 或 node:child_process 生成的进程未被杀死时,Bun 会自动杀死它们并在控制台打印消息。防止超时测试后僵尸进程残留后台。
测试修饰符
test.skip
使用 test.skip 跳过单个测试。跳过的测试不会被执行。

math.test.ts
import { expect, test } from "bun:test";
test.skip("wat", () => {
// TODO: 修复此处
expect(0.1 + 0.2).toEqual(0.3);
});
test.todo
使用 test.todo 标记测试为待办状态,这些测试不会被执行。

math.test.ts
import { expect, test } from "bun:test";
test.todo("修复此处", () => {
myTestFunction();
});
要执行 todo 测试并找出哪些测试已通过,可使用 bun test --todo。
my.test.ts:
✗ 未实现功能
^ 该测试被标记为 todo,且通过。请移除 `.todo` 或确认测试是否正确。
0 通过
1 失败
1 次 expect() 调用
启用此参数时,失败的 todo 测试不会导致错误,但通过的 todo 测试将被标记为失败,方便你移除 todo 标记或修复测试。
test.only
运行特定测试或测试套件,使用 test.only() 或 describe.only()。

example.test.ts
import { test, describe } from "bun:test";
test("测试 #1", () => {
// 不运行
});
test.only("测试 #2", () => {
// 运行
});
describe.only("仅此", () => {
test("测试 #3", () => {
// 运行
});
});
以下命令只会执行测试 #2 和 #3。
以下命令会执行测试 #1、#2 和 #3。
test.if
使用 test.if() 有条件地运行测试。当条件为真时运行。适用于仅在特定架构或操作系统上运行的测试。

example.test.ts
test.if(Math.random() > 0.5)("有一半概率运行", () => {
// ...
});
const macOS = process.platform === "darwin";
test.if(macOS)("仅在 macOS 运行", () => {
// 仅在 macOS 运行
});
test.skipIf
条件跳过测试,使用 test.skipIf() 或 describe.skipIf()。

example.test.ts
const macOS = process.platform === "darwin";
test.skipIf(macOS)("仅在非 macOS 运行", () => {
// 在非 macOS 运行
});
test.todoIf
条件标记测试为 TODO,使用 test.todoIf() 或 describe.todoIf()。切换 skipIf 或 todoIf 可区分“该目标无效”和“计划但未实现”的意图。

example.test.ts
const macOS = process.platform === "darwin";
// TODO: 目前只在 Linux 上实现。
test.todoIf(macOS)("运行在 posix 系统上", () => {
// 在非 macOS 运行
});
test.failing
当你知道测试当前会失败,但想跟踪它并在它开始通过时获取通知,使用 test.failing()。该修饰符对测试结果取反:
- 标有
.failing() 的失败测试会被视为通过
- 标有
.failing() 的通过测试会失败(并提示该测试现在通过,应修复)

math.test.ts
// 因测试故意失败,这里会通过
test.failing("数学有问题", () => {
expect(0.1 + 0.2).toBe(0.3); // 由于浮点精度失败
});
// 测试通过,但期望失败,因此测试失败并提示修复
test.failing("修复了 bug", () => {
expect(1 + 1).toBe(2); // 通过,但我们预期失败
});
适合跟踪你计划以后修复的已知错误,或用于测试驱动开发。
条件测试用于 describe 块
条件修饰符 .if()、.skipIf() 和 .todoIf() 也可以用于 describe 块,影响套件内的所有测试:

example.test.ts
const isMacOS = process.platform === "darwin";
// 仅在 macOS 上运行整个套件
describe.if(isMacOS)("macOS 特有功能", () => {
test("特性 A", () => {
// 仅在 macOS 运行
});
test("特性 B", () => {
// 仅在 macOS 运行
});
});
// 在 Windows 上跳过整个套件
describe.skipIf(process.platform === "win32")("Unix 功能", () => {
test("特性 C", () => {
// Windows 上跳过
});
});
// 在 Linux 上将整个套件标记为 TODO
describe.todoIf(process.platform === "linux")("即将支持 Linux", () => {
test("特性 D", () => {
// Linux 上标记为 TODO
});
});
参数化测试
test.each 和 describe.each
使用 test.each 可针对多个数据集运行相同测试。每个测试用例都会运行一次。

math.test.ts
const cases = [
[1, 2, 3],
[3, 4, 7],
];
test.each(cases)("%p + %p 应该是 %p", (a, b, expected) => {
expect(a + b).toBe(expected);
});
也可以用 describe.each 创建针对每个测试用例运行一次的参数化套件:

sum.test.ts
describe.each([
[1, 2, 3],
[3, 4, 7],
])("add(%i, %i)", (a, b, expected) => {
test(`返回 ${expected}`, () => {
expect(a + b).toBe(expected);
});
test(`结果大于每个值`, () => {
expect(a + b).toBeGreaterThan(a);
expect(a + b).toBeGreaterThan(b);
});
});
参数传递
如何将参数传递给测试函数,依赖于测试用例的结构:
- 如果表格行是数组(例如
[1, 2, 3]),则每个元素作为单独参数传入
- 如果行不是数组(如对象),则作为单个参数传入

example.test.ts
// 数组项作为单独参数传入
test.each([
[1, 2, 3],
[4, 5, 9],
])("add(%i, %i) = %i", (a, b, expected) => {
expect(a + b).toBe(expected);
});
// 对象项作为单个参数传入
test.each([
{ a: 1, b: 2, expected: 3 },
{ a: 4, b: 5, expected: 9 },
])("add($a, $b) = $expected", data => {
expect(data.a + data.b).toBe(data.expected);
});
格式化占位符
测试标题格式化支持以下占位符:
| 占位符 | 描述 |
|---|
%p | pretty-format 美化输出 |
%s | 字符串 |
%d | 数字 |
%i | 整数 |
%f | 浮点数 |
%j | JSON |
%o | 对象 |
%# | 测试用例索引 |
%% | 单个百分号 (%) |

example.test.ts
// 基本占位符
test.each([
["hello", 123],
["world", 456],
])("字符串: %s, 数字: %i", (str, num) => {
// "字符串: hello, 数字: 123"
// "字符串: world, 数字: 456"
});
// %p 用于 pretty-format 输出
test.each([
[{ name: "Alice" }, { a: 1, b: 2 }],
[{ name: "Bob" }, { x: 5, y: 10 }],
])("用户 %p,数据 %p", (user, data) => {
// "用户 { name: 'Alice' },数据 { a: 1, b: 2 }"
// "用户 { name: 'Bob' },数据 { x: 5, y: 10 }"
});
// %# 用于索引
test.each(["apple", "banana"])("水果 #%# 是 %s", fruit => {
// "水果 #0 是 apple"
// "水果 #1 是 banana"
});
断言计数
Bun 支持验证测试期间调用的断言数量:
expect.hasAssertions()
使用 expect.hasAssertions() 验证测试中至少调用过一次断言:

example.test.ts
test("异步工作调用断言", async () => {
expect.hasAssertions(); // 如果没有断言调用,测试失败
const data = await fetchData();
expect(data).toBeDefined();
});
这对于异步测试尤其有用,确保你的断言真正执行了。
expect.assertions(count)
使用 expect.assertions(count) 验证测试中调用了指定数量的断言:

example.test.ts
test("恰好两个断言", () => {
expect.assertions(2); // 如果断言不是精确2个则失败
expect(1 + 1).toBe(2);
expect("hello").toContain("ell");
});
尤其适合复杂异步代码中确保所有断言都执行。
类型测试
Bun 包含用于测试 TypeScript 类型的 expectTypeOf,兼容 Vitest。
expectTypeOf
这些函数在运行时不执行任何操作 —— 你需要单独运行 TypeScript 来验证类型检查。
expectTypeOf 函数提供类型级别的断言,由 TypeScript 的类型检查器检查。测试类型的方法:
- 使用
expectTypeOf 编写类型断言
- 运行
bunx tsc --noEmit 检查类型是否正确

example.test.ts
import { expectTypeOf } from "bun:test";
// 基本类型断言
expectTypeOf<string>().toEqualTypeOf<string>();
expectTypeOf(123).toBeNumber();
expectTypeOf("hello").toBeString();
// 对象类型匹配
expectTypeOf({ a: 1, b: "hello" }).toMatchObjectType<{ a: number }>();
// 函数类型
function greet(name: string): string {
return `Hello ${name}`;
}
expectTypeOf(greet).toBeFunction();
expectTypeOf(greet).parameters.toEqualTypeOf<[string]>();
expectTypeOf(greet).returns.toEqualTypeOf<string>();
// 数组类型
expectTypeOf([1, 2, 3]).items.toBeNumber();
// Promise 类型
expectTypeOf(Promise.resolve(42)).resolves.toBeNumber();
完整的 expectTypeOf 匹配器文档请参考 API 参考。
匹配器
Bun 已实现以下匹配器。完全兼容 Jest 正在规划中;进度跟踪。
基本匹配器
| 状态 | 匹配器 |
|---|
| ✅ | .not |
| ✅ | .toBe() |
| ✅ | .toEqual() |
| ✅ | .toBeNull() |
| ✅ | .toBeUndefined() |
| ✅ | .toBeNaN() |
| ✅ | .toBeDefined() |
| ✅ | .toBeFalsy() |
| ✅ | .toBeTruthy() |
| ✅ | .toStrictEqual() |
字符串和数组匹配器
| 状态 | 匹配器 |
|---|
| ✅ | .toContain() |
| ✅ | .toHaveLength() |
| ✅ | .toMatch() |
| ✅ | .toContainEqual() |
| ✅ | .stringContaining() |
| ✅ | .stringMatching() |
| ✅ | .arrayContaining() |
对象匹配器
| 状态 | 匹配器 |
|---|
| ✅ | .toHaveProperty() |
| ✅ | .toMatchObject() |
| ✅ | .toContainAllKeys() |
| ✅ | .toContainValue() |
| ✅ | .toContainValues() |
| ✅ | .toContainAllValues() |
| ✅ | .toContainAnyValues() |
| ✅ | .objectContaining() |
数字匹配器
| 状态 | 匹配器 |
|---|
| ✅ | .toBeCloseTo() |
| ✅ | .closeTo() |
| ✅ | .toBeGreaterThan() |
| ✅ | .toBeGreaterThanOrEqual() |
| ✅ | .toBeLessThan() |
| ✅ | .toBeLessThanOrEqual() |
函数和类匹配器
| 状态 | 匹配器 |
|---|
| ✅ | .toThrow() |
| ✅ | .toBeInstanceOf() |
Promise 匹配器
| 状态 | 匹配器 |
|---|
| ✅ | .resolves() |
| ✅ | .rejects() |
模拟函数匹配器
| 状态 | 匹配器 |
|---|
| ✅ | .toHaveBeenCalled() |
| ✅ | .toHaveBeenCalledTimes() |
| ✅ | .toHaveBeenCalledWith() |
| ✅ | .toHaveBeenLastCalledWith() |
| ✅ | .toHaveBeenNthCalledWith() |
| ✅ | .toHaveReturned() |
| ✅ | .toHaveReturnedTimes() |
| ✅ | .toHaveReturnedWith() |
| ✅ | .toHaveLastReturnedWith() |
| ✅ | .toHaveNthReturnedWith() |
快照匹配器
| 状态 | 匹配器 |
|---|
| ✅ | .toMatchSnapshot() |
| ✅ | .toMatchInlineSnapshot() |
| ✅ | .toThrowErrorMatchingSnapshot() |
| ✅ | .toThrowErrorMatchingInlineSnapshot() |
工具匹配器
| 状态 | 匹配器 |
|---|
| ✅ | .extend |
| ✅ | .anything() |
| ✅ | .any() |
| ✅ | .assertions() |
| ✅ | .hasAssertions() |
未实现
| 状态 | 匹配器 |
|---|
| ❌ | .addSnapshotSerializer() |
最佳实践
使用描述性的测试名称

example.test.ts
// 好的示例
test("应计算多个商品含税总价", () => {
// 测试实现
});
// 避免
test("价格计算", () => {
// 测试实现
});
分组相关测试

auth.test.ts
describe("用户认证", () => {
describe("有效凭证时", () => {
test("应返回用户数据", () => {
// 测试实现
});
test("应设置认证令牌", () => {
// 测试实现
});
});
describe("无效凭证时", () => {
test("应抛出认证错误", () => {
// 测试实现
});
});
});
使用合适的匹配器

auth.test.ts
// 好的示例:使用具体匹配器
expect(users).toHaveLength(3);
expect(user.email).toContain("@");
expect(response.status).toBeGreaterThanOrEqual(200);
// 避免:将逻辑表达式用 toBe 包裹
expect(users.length === 3).toBe(true);
expect(user.email.includes("@")).toBe(true);
expect(response.status >= 200).toBe(true);
测试错误情况

example.test.ts
test("无效输入应抛出错误", () => {
expect(() => {
validateEmail("not-an-email");
}).toThrow("Invalid email format");
});
test("应处理异步错误", async () => {
await expect(async () => {
await fetchUser("invalid-id");
}).rejects.toThrow("User not found");
});
使用初始化和清理

example.test.ts
import { beforeEach, afterEach, test } from "bun:test";
let testUser;
beforeEach(() => {
testUser = createTestUser();
});
afterEach(() => {
cleanupTestUser(testUser);
});
test("应更新用户资料", () => {
// 在测试中使用 testUser
});