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整理維護