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

基本用法

定义一个简单的测试:
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79math.test.ts
import { expect, test } from "bun:test";

test("2 + 2", () => {
  expect(2 + 2).toBe(4);
});

测试分组

使用 describe 将测试分组到测试套件中。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79math.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);
  });
});

异步测试

测试可以是异步的。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79math.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 回调参数,必须调用它,否则测试将挂起。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79math.test.ts
import { expect, test } from "bun:test";

test("2 * 2", done => {
  Promise.resolve(2 * 2).then(result => {
    expect(result).toEqual(4);
    done();
  });
});

超时

可通过给 test 传入第三个参数数字来指定单个测试的超时时间(毫秒)。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79math.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 选项可在测试失败时自动重试。只要在指定的尝试次数内成功,测试即通过。适合不稳定、偶尔失败的测试。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.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次重复)。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.test.ts
import { test } from "bun:test";

test(
  "确保测试稳定",
  () => {
    expect(Math.random()).toBeLessThan(1);
  },
  { repeats: 20 }, // 总计运行21次(1次初始 + 20次重复)
);
同一个测试不能同时使用 retryrepeats

🧟 僵尸进程清理器

当测试超时且通过 Bun.spawnBun.spawnSyncnode:child_process 生成的进程未被杀死时,Bun 会自动杀死它们并在控制台打印消息。防止超时测试后僵尸进程残留后台。

测试修饰符

test.skip

使用 test.skip 跳过单个测试。跳过的测试不会被执行。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79math.test.ts
import { expect, test } from "bun:test";

test.skip("wat", () => {
  // TODO: 修复此处
  expect(0.1 + 0.2).toEqual(0.3);
});

test.todo

使用 test.todo 标记测试为待办状态,这些测试不会被执行。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79math.test.ts
import { expect, test } from "bun:test";

test.todo("修复此处", () => {
  myTestFunction();
});
要执行 todo 测试并找出哪些测试已通过,可使用 bun test --todo
terminal
bun test --todo
my.test.ts:
✗ 未实现功能
  ^ 该测试被标记为 todo,且通过。请移除 `.todo` 或确认测试是否正确。

 0 通过
 1 失败
 1 次 expect() 调用
启用此参数时,失败的 todo 测试不会导致错误,但通过的 todo 测试将被标记为失败,方便你移除 todo 标记或修复测试。

test.only

运行特定测试或测试套件,使用 test.only()describe.only()
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.test.ts
import { test, describe } from "bun:test";

test("测试 #1", () => {
  // 不运行
});

test.only("测试 #2", () => {
  // 运行
});

describe.only("仅此", () => {
  test("测试 #3", () => {
    // 运行
  });
});
以下命令只会执行测试 #2 和 #3。
terminal
bun test --only
以下命令会执行测试 #1、#2 和 #3。
terminal
bun test

test.if

使用 test.if() 有条件地运行测试。当条件为真时运行。适用于仅在特定架构或操作系统上运行的测试。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.test.ts
test.if(Math.random() > 0.5)("有一半概率运行", () => {
  // ...
});

const macOS = process.platform === "darwin";
test.if(macOS)("仅在 macOS 运行", () => {
  // 仅在 macOS 运行
});

test.skipIf

条件跳过测试,使用 test.skipIf()describe.skipIf()
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.test.ts
const macOS = process.platform === "darwin";

test.skipIf(macOS)("仅在非 macOS 运行", () => {
  // 在非 macOS 运行
});

test.todoIf

条件标记测试为 TODO,使用 test.todoIf()describe.todoIf()。切换 skipIftodoIf 可区分“该目标无效”和“计划但未实现”的意图。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.test.ts
const macOS = process.platform === "darwin";

// TODO: 目前只在 Linux 上实现。
test.todoIf(macOS)("运行在 posix 系统上", () => {
  // 在非 macOS 运行
});

test.failing

当你知道测试当前会失败,但想跟踪它并在它开始通过时获取通知,使用 test.failing()。该修饰符对测试结果取反:
  • 标有 .failing() 的失败测试会被视为通过
  • 标有 .failing() 的通过测试会失败(并提示该测试现在通过,应修复)
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79math.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 块,影响套件内的所有测试:
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.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.eachdescribe.each

使用 test.each 可针对多个数据集运行相同测试。每个测试用例都会运行一次。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79math.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 创建针对每个测试用例运行一次的参数化套件:
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79sum.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]),则每个元素作为单独参数传入
  • 如果行不是数组(如对象),则作为单个参数传入
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.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);
});

格式化占位符

测试标题格式化支持以下占位符:
占位符描述
%ppretty-format 美化输出
%s字符串
%d数字
%i整数
%f浮点数
%jJSON
%o对象
%#测试用例索引
%%单个百分号 (%)

示例

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.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() 验证测试中至少调用过一次断言:
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.test.ts
test("异步工作调用断言", async () => {
  expect.hasAssertions(); // 如果没有断言调用,测试失败

  const data = await fetchData();
  expect(data).toBeDefined();
});
这对于异步测试尤其有用,确保你的断言真正执行了。

expect.assertions(count)

使用 expect.assertions(count) 验证测试中调用了指定数量的断言:
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.test.ts
test("恰好两个断言", () => {
  expect.assertions(2); // 如果断言不是精确2个则失败

  expect(1 + 1).toBe(2);
  expect("hello").toContain("ell");
});
尤其适合复杂异步代码中确保所有断言都执行。

类型测试

Bun 包含用于测试 TypeScript 类型的 expectTypeOf,兼容 Vitest。

expectTypeOf

这些函数在运行时不执行任何操作 —— 你需要单独运行 TypeScript 来验证类型检查。
expectTypeOf 函数提供类型级别的断言,由 TypeScript 的类型检查器检查。测试类型的方法:
  1. 使用 expectTypeOf 编写类型断言
  2. 运行 bunx tsc --noEmit 检查类型是否正确
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.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()

最佳实践

使用描述性的测试名称

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.test.ts
// 好的示例
test("应计算多个商品含税总价", () => {
  // 测试实现
});

// 避免
test("价格计算", () => {
  // 测试实现
});

分组相关测试

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79auth.test.ts
describe("用户认证", () => {
  describe("有效凭证时", () => {
    test("应返回用户数据", () => {
      // 测试实现
    });

    test("应设置认证令牌", () => {
      // 测试实现
    });
  });

  describe("无效凭证时", () => {
    test("应抛出认证错误", () => {
      // 测试实现
    });
  });
});

使用合适的匹配器

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79auth.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);

测试错误情况

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.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");
});

使用初始化和清理

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79example.test.ts
import { beforeEach, afterEach, test } from "bun:test";

let testUser;

beforeEach(() => {
  testUser = createTestUser();
});

afterEach(() => {
  cleanupTestUser(testUser);
});

test("应更新用户资料", () => {
  // 在测试中使用 testUser
});