使用从内置 bun:test 模块导入的类 Jest API 定义测试。长期来看,Bun 旨在实现完整的 Jest 兼容性;目前,支持有限的 expect 匹配器集合。
基本用法
定义一个简单的测试:
import { expect, test } from "bun:test";
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});分组测试
测试可以使用 describe 分组到套件中。
import { expect, test, describe } from "bun:test";
describe("算术", () => {
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
test("2 * 2", () => {
expect(2 * 2).toBe(4);
});
});异步测试
测试可以是异步的。
import { expect, test } from "bun:test";
test("2 * 2", async () => {
const result = await Promise.resolve(2 * 2);
expect(result).toEqual(4);
});或者,使用 done 回调来信号完成。如果在测试定义中包含 done 回调作为参数,你必须调用它,否则测试将挂起。
import { expect, test } from "bun:test";
test("2 * 2", done => {
Promise.resolve(2 * 2).then(result => {
expect(result).toEqual(4);
done();
});
});超时
通过将数字作为第三个参数传递给 test,可以选择指定每个测试的超时时间(以毫秒为单位)。
import { test } from "bun:test";
test("wat", async () => {
const data = await slowOperation();
expect(data).toBe(42);
}, 500); // 测试必须在 <500ms 内运行在 bun:test 中,测试超时会抛出不可捕获的异常以强制测试停止运行并失败。我们还会终止测试中生成的任何子进程,以避免在后台留下僵尸进程。
如果未通过此超时选项或 jest.setDefaultTimeout() 覆盖,每个测试的默认超时时间为 5000 毫秒(5 秒)。
重试和重复
test.retry
使用 retry 选项在测试失败时自动重试。如果测试在指定次数内成功,则测试通过。这对于可能间歇性失败的不稳定测试很有用。
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 次重复)。
import { test } from "bun:test";
test(
"确保测试稳定",
() => {
expect(Math.random()).toBeLessThan(1);
},
{ repeats: 20 }, // 总共运行 21 次(1 次初始 + 20 次重复)
);NOTE
你不能在同一测试上同时使用 `retry` 和 `repeats`。🧟 僵尸进程杀手
当测试超时且通过 Bun.spawn、Bun.spawnSync 或 node:child_process 在测试中生成的进程未被终止时,它们将自动被终止,并且消息将记录到控制台。这可以防止僵尸进程在超时测试后在后台徘徊。
测试修饰符
test.skip
使用 test.skip 跳过单个测试。这些测试将不会运行。
import { expect, test } from "bun:test";
test.skip("wat", () => {
// TODO: 修复这个
expect(0.1 + 0.2).toEqual(0.3);
});test.todo
使用 test.todo 将测试标记为待办。这些测试将不会运行。
import { expect, test } from "bun:test";
test.todo("fix this", () => {
myTestFunction();
});要运行待办测试并查找任何通过的测试,请使用 bun test --todo。
bun test --todomy.test.ts:
✗ unimplemented feature
^ 此测试标记为待办但通过了。删除 `.todo` 或检查测试是否正确。
0 pass
1 fail
1 expect() calls使用此标志时,失败的待办测试不会导致错误,但通过的待办测试将标记为失败,因此你可以删除待办标记或修复测试。
test.only
要运行特定测试或套件,请使用 test.only() 或 describe.only()。
import { test, describe } from "bun:test";
test("test #1", () => {
// 不运行
});
test.only("test #2", () => {
// 运行
});
describe.only("only", () => {
test("test #3", () => {
// 运行
});
});以下命令将仅执行测试 #2 和 #3。
bun test --only以下命令将仅执行测试 #1、#2 和 #3。
bun testtest.if
要条件性地运行测试,请使用 test.if()。如果条件为真,则测试将运行。这对于仅应在特定架构或操作系统上运行的测试特别有用。
test.if(Math.random() > 0.5)("一半时间运行", () => {
// ...
});
const macOS = process.platform === "darwin";
test.if(macOS)("在 macOS 上运行", () => {
// 如果 macOS 则运行
});test.skipIf
要根据某些条件跳过测试,请使用 test.skipIf() 或 describe.skipIf()。
const macOS = process.platform === "darwin";
test.skipIf(macOS)("在非 macOS 上运行", () => {
// 如果 *非* macOS 则运行
});test.todoIf
如果你想将测试标记为待办,请使用 test.todoIf() 或 describe.todoIf()。仔细选择 skipIf 或 todoIf 可以显示区别,例如,"对此目标无效" 和 "计划但尚未实现" 之间的意图差异。
const macOS = process.platform === "darwin";
// TODO: 我们目前只为 Linux 实现了这个。
test.todoIf(macOS)("在 posix 上运行", () => {
// 如果 *非* macOS 则运行
});test.failing
当你知道测试目前失败但想要跟踪它并在开始通过时收到通知时,请使用 test.failing()。这会反转测试结果:
- 标记为
.failing()的失败测试将通过 - 标记为
.failing()的通过测试将失败(带有消息指示它现在通过并应修复)
// 这将通过,因为测试按预期失败
test.failing("数学已损坏", () => {
expect(0.1 + 0.2).toBe(0.3); // 由于浮点精度而失败
});
// 这将失败,并带有消息指示测试现在通过
test.failing("已修复的错误", () => {
expect(1 + 1).toBe(2); // 通过,但我们预期它会失败
});这对于跟踪你计划稍后修复的已知错误,或实现测试驱动开发很有用。
Describe 块的条件测试
条件修饰符 .if()、.skipIf() 和 .todoIf() 也可以应用于 describe 块,影响套件中的所有测试:
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 上将整个套件标记为待办
describe.todoIf(process.platform === "linux")("即将推出的 Linux 支持", () => {
test("功能 D", () => {
// 在 Linux 上标记为待办
});
});参数化测试
test.each 和 describe.each
要使用多组数据运行相同的测试,请使用 test.each。这会创建一个参数化测试,为提供的每个测试用例运行一次。
const cases = [
[1, 2, 3],
[3, 4, 7],
];
test.each(cases)("%p + %p 应该等于 %p", (a, b, expected) => {
expect(a + b).toBe(expected);
});你也可以使用 describe.each 创建一个参数化套件,为每个测试用例运行一次:
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]),则每个元素作为单独的参数传递 - 如果行不是数组(如对象),则作为单个参数传递
// 数组项作为单独参数传递
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 | String |
%d | Number |
%i | Integer |
%f | Floating point |
%j | JSON |
%o | Object |
%# | 测试用例的索引 |
%% | 单个百分号 (%) |
示例
// 基本说明符
test.each([
["hello", 123],
["world", 456],
])("string: %s, number: %i", (str, num) => {
// "string: hello, number: 123"
// "string: world, number: 456"
});
// %p 用于 pretty-format 输出
test.each([
[{ name: "Alice" }, { a: 1, b: 2 }],
[{ name: "Bob" }, { x: 5, y: 10 }],
])("user %p with data %p", (user, data) => {
// "user { name: 'Alice' } with data { a: 1, b: 2 }"
// "user { name: 'Bob' } with data { x: 5, y: 10 }"
});
// %# 用于索引
test.each(["apple", "banana"])("fruit #%# is %s", fruit => {
// "fruit #0 is apple"
// "fruit #1 is banana"
});断言计数
Bun 支持验证在测试期间调用了特定数量的断言:
expect.hasAssertions()
使用 expect.hasAssertions() 验证在测试期间至少调用了一个断言:
test("异步工作调用断言", async () => {
expect.hasAssertions(); // 如果没有调用断言将失败
const data = await fetchData();
expect(data).toBeDefined();
});这对于异步测试特别有用,可确保你的断言实际运行。
expect.assertions(count)
使用 expect.assertions(count) 验证在测试期间调用了特定数量的断言:
test("正好两个断言", () => {
expect.assertions(2); // 如果没有正好调用 2 个断言将失败
expect(1 + 1).toBe(2);
expect("hello").toContain("ell");
});这有助于确保所有断言都运行,特别是在具有多个代码路径的复杂异步代码中。
类型测试
Bun 包括 expectTypeOf 用于测试 TypeScript 类型,与 Vitest 兼容。
expectTypeOf
expectTypeOf 函数提供由 TypeScript 类型检查器检查的类型级断言。要测试你的类型:
- 使用
expectTypeOf编写类型断言 - 运行
bunx tsc --noEmit检查类型是否正确
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() |
最佳实践
使用描述性测试名称
// 好
test("应该计算包含税费的多个商品总价", () => {
// 测试实现
});
// 避免
test("价格计算", () => {
// 测试实现
});分组相关测试
describe("用户认证", () => {
describe("使用有效凭据", () => {
test("应该返回用户数据", () => {
// 测试实现
});
test("应该设置认证令牌", () => {
// 测试实现
});
});
describe("使用无效凭据", () => {
test("应该抛出认证错误", () => {
// 测试实现
});
});
});使用适当的匹配器
// 好:使用特定匹配器
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);测试错误条件
test("应该为无效输入抛出错误", () => {
expect(() => {
validateEmail("not-an-email");
}).toThrow("无效电子邮件格式");
});
test("应该处理异步错误", async () => {
await expect(async () => {
await fetchUser("invalid-id");
}).rejects.toThrow("用户未找到");
});使用设置和清理
import { beforeEach, afterEach, test } from "bun:test";
let testUser;
beforeEach(() => {
testUser = createTestUser();
});
afterEach(() => {
cleanupTestUser(testUser);
});
test("应该更新用户个人资料", () => {
// 在测试中使用 testUser
});