Skip to content

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

基本用法 ​

定义一个简单的测试:

math.test.ts
ts
import { expect, test } from "bun:test";

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

分组测试 ​

测试可以使用 describe 分组到套件中。

math.test.ts
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
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
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
ts
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 选项在测试失败时自动重试。如果测试在指定次数内成功,则测试通过。这对于可能间歇性失败的不稳定测试很有用。

example.test.ts
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
ts
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 跳过单个测试。这些测试将不会运行。

math.test.ts
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
ts
import { expect, test } from "bun:test";

test.todo("fix this", () => {
  myTestFunction();
});

要运行待办测试并查找任何通过的测试,请使用 bun test --todo。

bash
bun test --todo
my.test.ts:
✗ unimplemented feature
  ^ 此测试标记为待办但通过了。删除 `.todo` 或检查测试是否正确。

 0 pass
 1 fail
 1 expect() calls

使用此标志时,失败的待办测试不会导致错误,但通过的待办测试将标记为失败,因此你可以删除待办标记或修复测试。

test.only ​

要运行特定测试或套件,请使用 test.only() 或 describe.only()。

example.test.ts
ts
import { test, describe } from "bun:test";

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

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

describe.only("only", () => {
  test("test #3", () => {
    // 运行
  });
});

以下命令将仅执行测试 #2 和 #3。

bash
bun test --only

以下命令将仅执行测试 #1、#2 和 #3。

bash
bun test

test.if ​

要条件性地运行测试,请使用 test.if()。如果条件为真,则测试将运行。这对于仅应在特定架构或操作系统上运行的测试特别有用。

example.test.ts
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
ts
const macOS = process.platform === "darwin";

test.skipIf(macOS)("在非 macOS 上运行", () => {
  // 如果 *非* macOS 则运行
});

test.todoIf ​

如果你想将测试标记为待办,请使用 test.todoIf() 或 describe.todoIf()。仔细选择 skipIf 或 todoIf 可以显示区别,例如,"对此目标无效" 和 "计划但尚未实现" 之间的意图差异。

example.test.ts
ts
const macOS = process.platform === "darwin";

// TODO: 我们目前只为 Linux 实现了这个。
test.todoIf(macOS)("在 posix 上运行", () => {
  // 如果 *非* macOS 则运行
});

test.failing ​

当你知道测试目前失败但想要跟踪它并在开始通过时收到通知时,请使用 test.failing()。这会反转测试结果:

  • 标记为 .failing() 的失败测试将通过
  • 标记为 .failing() 的通过测试将失败(带有消息指示它现在通过并应修复)
ts
// 这将通过,因为测试按预期失败
test.failing("数学已损坏", () => {
  expect(0.1 + 0.2).toBe(0.3); // 由于浮点精度而失败
});

// 这将失败,并带有消息指示测试现在通过
test.failing("已修复的错误", () => {
  expect(1 + 1).toBe(2); // 通过,但我们预期它会失败
});

这对于跟踪你计划稍后修复的已知错误,或实现测试驱动开发很有用。

Describe 块的条件测试 ​

条件修饰符 .if()、.skipIf() 和 .todoIf() 也可以应用于 describe 块,影响套件中的所有测试:

example.test.ts
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 上将整个套件标记为待办
describe.todoIf(process.platform === "linux")("即将推出的 Linux 支持", () => {
  test("功能 D", () => {
    // 在 Linux 上标记为待办
  });
});

参数化测试 ​

test.each 和 describe.each ​

要使用多组数据运行相同的测试,请使用 test.each。这会创建一个参数化测试,为提供的每个测试用例运行一次。

math.test.ts
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
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
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
%sString
%dNumber
%iInteger
%fFloating point
%jJSON
%oObject
%#测试用例的索引
%%单个百分号 (%)

示例 ​

example.test.ts
ts
// 基本说明符
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() 验证在测试期间至少调用了一个断言:

example.test.ts
ts
test("异步工作调用断言", async () => {
  expect.hasAssertions(); // 如果没有调用断言将失败

  const data = await fetchData();
  expect(data).toBeDefined();
});

这对于异步测试特别有用,可确保你的断言实际运行。

expect.assertions(count) ​

使用 expect.assertions(count) 验证在测试期间调用了特定数量的断言:

example.test.ts
ts
test("正好两个断言", () => {
  expect.assertions(2); // 如果没有正好调用 2 个断言将失败

  expect(1 + 1).toBe(2);
  expect("hello").toContain("ell");
});

这有助于确保所有断言都运行,特别是在具有多个代码路径的复杂异步代码中。

类型测试 ​

Bun 包括 expectTypeOf 用于测试 TypeScript 类型,与 Vitest 兼容。

expectTypeOf ​

expectTypeOf 函数提供由 TypeScript 类型检查器检查的类型级断言。要测试你的类型:

  1. 使用 expectTypeOf 编写类型断言
  2. 运行 bunx tsc --noEmit 检查类型是否正确
example.test.ts
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
ts
// 好
test("应该计算包含税费的多个商品总价", () => {
  // 测试实现
});

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

分组相关测试 ​

auth.test.ts
ts
describe("用户认证", () => {
  describe("使用有效凭据", () => {
    test("应该返回用户数据", () => {
      // 测试实现
    });

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

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

使用适当的匹配器 ​

auth.test.ts
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
ts
test("应该为无效输入抛出错误", () => {
  expect(() => {
    validateEmail("not-an-email");
  }).toThrow("无效电子邮件格式");
});

test("应该处理异步错误", async () => {
  await expect(async () => {
    await fetchUser("invalid-id");
  }).rejects.toThrow("用户未找到");
});

使用设置和清理 ​

example.test.ts
ts
import { beforeEach, afterEach, test } from "bun:test";

let testUser;

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

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

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