使用从内置 bun:test 模块导入的类似 Jest 的 API 定义测试。从长远来看,Bun 旨在实现完全的 Jest 兼容性;目前,仅支持有限的 expect 匹配器集合。
基本用法
定义一个简单测试:
math.test.tsimport { expect, test } from "bun:test";
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
分组测试
测试可以用 describe 分组成套件。
math.test.tsimport { expect, test, describe } from "bun:test";
describe("arithmetic", () => {
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
test("2 * 2", () => {
expect(2 * 2).toBe(4);
});
});
异步测试
测试可以是异步的。
math.test.tsimport { expect, test } from "bun:test";
test("2 * 2", async () => {
const result = await Promise.resolve(2 * 2);
expect(result).toEqual(4);
});
或者,使用 done 回调来表示完成。如果您在测试定义中包含 done 回调作为参数,您必须调用它,否则测试将挂起。
math.test.tsimport { expect, test } from "bun:test";
test("2 * 2", done => {
Promise.resolve(2 * 2).then(result => {
expect(result).toEqual(4);
done();
});
});
通过将数字作为第三个参数传递给 test,可以选择性地指定每个测试的超时时间(以毫秒为单位)。
math.test.tsimport { 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.tsimport { test } from "bun:test";
test(
"flaky network request",
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.tsimport { test } from "bun:test";
test(
"ensure test is stable",
() => {
expect(Math.random()).toBeLessThan(1);
},
{ repeats: 20 }, // 总共运行 21 次(1 次初始 + 20 次重复)
);
您不能在同一测试中同时使用 retry 和 repeats。
🧟 僵尸进程杀手
当测试超时并且通过 Bun.spawn、Bun.spawnSync 或 node:child_process 在测试中生成的进程没有被杀死时,它们将被自动杀死,并将消息记录到控制台。这可以防止超时测试后僵尸进程在后台潜伏。
测试修饰符
test.skip
使用 test.skip 跳过单个测试。这些测试不会运行。
math.test.tsimport { expect, test } from "bun:test";
test.skip("wat", () => {
// TODO: 修复这个问题
expect(0.1 + 0.2).toEqual(0.3);
});
test.todo
使用 test.todo 将测试标记为待办事项。这些测试不会运行。
math.test.tsimport { expect, test } from "bun:test";
test.todo("fix this", () => {
myTestFunction();
});
要运行待办测试并查找任何通过的测试,请使用 bun test --todo。
my.test.ts:
✗ unimplemented feature
^ this test is marked as todo but passes. Remove `.todo` or check that test is correct.
0 pass
1 fail
1 expect() calls
使用此标志,失败的待办测试不会导致错误,但通过的待办测试将被标记为失败,以便您可以删除待办标记或修复测试。
test.only
要运行特定测试或测试套件,请使用 test.only() 或 describe.only()。
example.test.tsimport { test, describe } from "bun:test";
test("test #1", () => {
// 不运行
});
test.only("test #2", () => {
// 运行
});
describe.only("only", () => {
test("test #3", () => {
// 运行
});
});
以下命令将仅执行测试 #2 和 #3。
以下命令将仅执行测试 #1、#2 和 #3。
test.if
要根据条件运行测试,请使用 test.if()。如果条件为真值,则测试将运行。这对于仅应在特定架构或操作系统上运行的测试特别有用。
example.test.tstest.if(Math.random() > 0.5)("runs half the time", () => {
// ...
});
const macOS = process.platform === "darwin";
test.if(macOS)("runs on macOS", () => {
// 如果 macOS 运行
});
test.skipIf
要基于某些条件跳过测试,请使用 test.skipIf() 或 describe.skipIf()。
example.test.tsconst macOS = process.platform === "darwin";
test.skipIf(macOS)("runs on non-macOS", () => {
// 如果*不是* macOS 运行
});
test.todoIf
如果要将测试标记为 TODO,请使用 test.todoIf() 或 describe.todoIf()。仔细选择 skipIf 或 todoIf 可以显示”对此目标无效”和”计划但尚未实现”之间的意图差异。
example.test.tsconst macOS = process.platform === "darwin";
// TODO: 我们只在 Linux 上实现了这个功能。
test.todoIf(macOS)("runs on posix", () => {
// 如果*不是* macOS 运行
});
test.failing
当您知道测试当前失败但希望跟踪它并在它开始通过时收到通知时,请使用 test.failing()。这会反转测试结果:
- 标记为
.failing() 的失败测试将通过
- 标记为
.failing() 的通过测试将失败(带有消息表明它现在通过了,应该修复)
math.test.ts// 这将通过,因为测试按预期失败
test.failing("math is broken", () => {
expect(0.1 + 0.2).toBe(0.3); // 由于浮点精度而失败
});
// 这将失败并显示测试现在通过的消息
test.failing("fixed bug", () => {
expect(1 + 1).toBe(2); // 通过,但我们期望它失败
});
这对于跟踪计划稍后修复的已知错误,或用于实现测试驱动开发很有用。
描述块的条件测试
条件修饰符 .if()、.skipIf() 和 .todoIf() 也可以应用于描述块,影响套件中的所有测试:
example.test.tsconst isMacOS = process.platform === "darwin";
// 仅在 macOS 上运行整个套件
describe.if(isMacOS)("macOS-specific features", () => {
test("feature A", () => {
// 仅在 macOS 上运行
});
test("feature B", () => {
// 仅在 macOS 上运行
});
});
// 在 Windows 上跳过整个套件
describe.skipIf(process.platform === "win32")("Unix features", () => {
test("feature C", () => {
// 在 Windows 上跳过
});
});
// 在 Linux 上将整个套件标记为 TODO
describe.todoIf(process.platform === "linux")("Upcoming Linux support", () => {
test("feature D", () => {
// 在 Linux 上标记为 TODO
});
});
参数化测试
test.each 和 describe.each
要使用多组数据运行相同的测试,请使用 test.each。这将创建一个参数化测试,为提供的每个测试用例运行一次。
math.test.tsconst cases = [
[1, 2, 3],
[3, 4, 7],
];
test.each(cases)("%p + %p should be %p", (a, b, expected) => {
expect(a + b).toBe(expected);
});
您也可以使用 describe.each 创建参数化套件,为每个测试用例运行一次:
sum.test.tsdescribe.each([
[1, 2, 3],
[3, 4, 7],
])("add(%i, %i)", (a, b, expected) => {
test(`returns ${expected}`, () => {
expect(a + b).toBe(expected);
});
test(`sum is greater than each value`, () => {
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 | String |
%d | Number |
%i | Integer |
%f | Floating point |
%j | JSON |
%o | Object |
%# | 测试用例的索引 |
%% | 单个百分号 (%) |
example.test.ts// 基本说明符
test.each([
["hello", 123],
["world", 456],
])("string: %s, number: %i", (str, num) => {
// "string: hello, number: 123"
// "string: world, number: 456"
});
// %p 用于漂亮格式输出
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() 验证测试期间至少调用了一个断言:
example.test.tstest("async work calls assertions", async () => {
expect.hasAssertions(); // 如果没有调用断言将失败
const data = await fetchData();
expect(data).toBeDefined();
});
这在异步测试中特别有用,以确保您的断言确实运行。
expect.assertions(count)
使用 expect.assertions(count) 验证测试期间调用了特定数量的断言:
example.test.tstest("exactly two assertions", () => {
expect.assertions(2); // 如果没有恰好调用 2 个断言将失败
expect(1 + 1).toBe(2);
expect("hello").toContain("ell");
});
这有助于确保所有断言都运行,特别是在具有多个代码路径的复杂异步代码中。
类型测试
Bun 包含 expectTypeOf 用于测试 TypeScript 类型,与 Vitest 兼容。
expectTypeOf
这些函数在运行时是无操作 - 您需要单独运行 TypeScript 来验证类型检查。
expectTypeOf 函数提供由 TypeScript 类型检查器检查的类型级断言。要测试您的类型:
- 使用 expectTypeOf 编写类型断言
- 运行
bunx tsc --noEmit 检查您的类型是否正确
example.test.tsimport { 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("should calculate total price including tax for multiple items", () => {
// 测试实现
});
// 避免
test("price calculation", () => {
// 测试实现
});
分组相关测试
auth.test.tsdescribe("User authentication", () => {
describe("with valid credentials", () => {
test("should return user data", () => {
// 测试实现
});
test("should set authentication token", () => {
// 测试实现
});
});
describe("with invalid credentials", () => {
test("should throw authentication error", () => {
// 测试实现
});
});
});
使用适当的匹配器
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.tstest("should throw error for invalid input", () => {
expect(() => {
validateEmail("not-an-email");
}).toThrow("Invalid email format");
});
test("should handle async errors", async () => {
await expect(async () => {
await fetchUser("invalid-id");
}).rejects.toThrow("User not found");
});
使用设置和清理
example.test.tsimport { beforeEach, afterEach, test } from "bun:test";
let testUser;
beforeEach(() => {
testUser = createTestUser();
});
afterEach(() => {
cleanupTestUser(testUser);
});
test("should update user profile", () => {
// 在测试中使用 testUser
});