Skip to content

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

基本用法

定义一个简单的测试:

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

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

分组测试

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

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);
  });
});

异步测试

测试可以是异步的。

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

test("2 * 2", async () => {
  const result = await Promise.resolve(2 * 2);
  expect(result).toEqual(4);
});

或者,使用 done 回调来信号完成。如果在测试定义中包含 done 回调作为参数,你必须调用它,否则测试将挂起。

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

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

超时

通过将数字作为第三个参数传递给 test,可以选择指定每个测试的超时时间(以毫秒为单位)。

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

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 次重复)。

ts
import { test } from "bun:test";

test(
  "确保测试稳定",
  () => {
    expect(Math.random()).toBeLessThan(1);
  },
  { repeats: 20 }, // 总共运行 21 次(1 次初始 + 20 次重复)
);

NOTE

你不能在同一测试上同时使用 `retry` 和 `repeats`。

🧟 僵尸进程杀手

当测试超时且通过 Bun.spawnBun.spawnSyncnode:child_process 在测试中生成的进程未被终止时,它们将自动被终止,并且消息将记录到控制台。这可以防止僵尸进程在超时测试后在后台徘徊。

测试修饰符

test.skip

使用 test.skip 跳过单个测试。这些测试将不会运行。

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

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

test.todo

使用 test.todo 将测试标记为待办。这些测试将不会运行。

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()

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()。如果条件为真,则测试将运行。这对于仅应在特定架构或操作系统上运行的测试特别有用。

ts
test.if(Math.random() > 0.5)("一半时间运行", () => {
  // ...
});

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

test.skipIf

要根据某些条件跳过测试,请使用 test.skipIf()describe.skipIf()

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

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

test.todoIf

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

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 块,影响套件中的所有测试:

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.eachdescribe.each

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

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 创建一个参数化套件,为每个测试用例运行一次:

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]),则每个元素作为单独的参数传递
  • 如果行不是数组(如对象),则作为单个参数传递
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
%#测试用例的索引
%%单个百分号 (%)

示例

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() 验证在测试期间至少调用了一个断言:

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

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

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

expect.assertions(count)

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

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 检查类型是否正确
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()

最佳实践

使用描述性测试名称

ts
// 好
test("应该计算包含税费的多个商品总价", () => {
  // 测试实现
});

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

分组相关测试

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

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

  describe("使用无效凭据", () => {
    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);

测试错误条件

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

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

使用设置和清理

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

let testUser;

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

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

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

Bun学习网由www.bunjs.com.cn整理维护