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

基本用法

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

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

分组测试

测试可以用 describe 分组成套件。
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2math.test.ts
import { expect, test, describe } from "bun:test";

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

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

异步测试

测试可以是异步的。
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2math.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/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2math.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/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2math.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/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.test.ts
import { 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 次重复)。
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.test.ts
import { test } from "bun:test";

test(
  "ensure test is stable",
  () => {
    expect(Math.random()).toBeLessThan(1);
  },
  { repeats: 20 }, // 总共运行 21 次(1 次初始 + 20 次重复)
);
您不能在同一测试中同时使用 retryrepeats

🧟 僵尸进程杀手

当测试超时并且通过 Bun.spawn、Bun.spawnSync 或 node:child_process 在测试中生成的进程没有被杀死时,它们将被自动杀死,并将消息记录到控制台。这可以防止超时测试后僵尸进程在后台潜伏。

测试修饰符

test.skip

使用 test.skip 跳过单个测试。这些测试不会运行。
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2math.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/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2math.test.ts
import { expect, test } from "bun:test";

test.todo("fix this", () => {
  myTestFunction();
});
要运行待办测试并查找任何通过的测试,请使用 bun test --todo
terminal
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()。
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.test.ts
import { test, describe } from "bun:test";

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

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

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

test.if

要根据条件运行测试,请使用 test.if()。如果条件为真值,则测试将运行。这对于仅应在特定架构或操作系统上运行的测试特别有用。
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.test.ts
test.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()。
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.test.ts
const macOS = process.platform === "darwin";

test.skipIf(macOS)("runs on non-macOS", () => {
  // 如果*不是* macOS 运行
});

test.todoIf

如果要将测试标记为 TODO,请使用 test.todoIf() 或 describe.todoIf()。仔细选择 skipIf 或 todoIf 可以显示”对此目标无效”和”计划但尚未实现”之间的意图差异。
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.test.ts
const macOS = process.platform === "darwin";

// TODO: 我们只在 Linux 上实现了这个功能。
test.todoIf(macOS)("runs on posix", () => {
  // 如果*不是* macOS 运行
});

test.failing

当您知道测试当前失败但希望跟踪它并在它开始通过时收到通知时,请使用 test.failing()。这会反转测试结果:
  • 标记为 .failing() 的失败测试将通过
  • 标记为 .failing() 的通过测试将失败(带有消息表明它现在通过了,应该修复)
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2math.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() 也可以应用于描述块,影响套件中的所有测试:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.test.ts
const 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。这将创建一个参数化测试,为提供的每个测试用例运行一次。
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2math.test.ts
const 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 创建参数化套件,为每个测试用例运行一次:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2sum.test.ts
describe.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]),则每个元素作为单独的参数传递
  • 如果行不是数组(如对象),则作为单个参数传递
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.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
%sString
%dNumber
%iInteger
%fFloating point
%jJSON
%oObject
%#测试用例的索引
%%单个百分号 (%)

示例

https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.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() 验证测试期间至少调用了一个断言:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.test.ts
test("async work calls assertions", async () => {
  expect.hasAssertions(); // 如果没有调用断言将失败

  const data = await fetchData();
  expect(data).toBeDefined();
});
这在异步测试中特别有用,以确保您的断言确实运行。

expect.assertions(count)

使用 expect.assertions(count) 验证测试期间调用了特定数量的断言:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.test.ts
test("exactly two assertions", () => {
  expect.assertions(2); // 如果没有恰好调用 2 个断言将失败

  expect(1 + 1).toBe(2);
  expect("hello").toContain("ell");
});
这有助于确保所有断言都运行,特别是在具有多个代码路径的复杂异步代码中。

类型测试

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

expectTypeOf

这些函数在运行时是无操作 - 您需要单独运行 TypeScript 来验证类型检查。
expectTypeOf 函数提供由 TypeScript 类型检查器检查的类型级断言。要测试您的类型:
  1. 使用 expectTypeOf 编写类型断言
  2. 运行 bunx tsc --noEmit 检查您的类型是否正确
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.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/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.test.ts
// 好的
test("should calculate total price including tax for multiple items", () => {
  // 测试实现
});

// 避免
test("price calculation", () => {
  // 测试实现
});

分组相关测试

https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2auth.test.ts
describe("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", () => {
      // 测试实现
    });
  });
});

使用适当的匹配器

https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2auth.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/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.test.ts
test("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");
});

使用设置和清理

https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2example.test.ts
import { beforeEach, afterEach, test } from "bun:test";

let testUser;

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

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

test("should update user profile", () => {
  // 在测试中使用 testUser
});