編寫測試
使用 Bun 相容 Jest 的 API 編寫測試,包括非同步測試、超時和測試修飾符
使用從內建 bun:test 模組匯入的類 Jest API 定義測試。從長遠來看,Bun 的目標是完全相容 Jest。
基本用法
定義測試:
import { expect, test } from "bun:test";
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
測試分組
使用 describe 將測試分組到測試套件中。
import { expect, test, describe } from "bun:test";
describe("算術", () => {
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
test("2 * 2", () => {
expect(2 * 2).toBe(4);
});
});
非同步測試
測試可以是非同步的。
import { expect, test } from "bun:test";
test("2 * 2", async () => {
const result = await Promise.resolve(2 * 2);
expect(result).toEqual(4);
});
或者,使用 done 回撥來表示完成。如果測試函式接收 done 引數,則必須呼叫它,否則測試會一直掛起。
import { expect, test } from "bun:test";
test("2 * 2", done => {
Promise.resolve(2 * 2).then(result => {
expect(result).toEqual(4);
done();
});
});
超時
可通過給 test 傳入第三個引數數字來指定單個測試的超時時間(毫秒)。
import { expect, test } from "bun:test";
test("wat", async () => {
const data = await slowOperation();
expect(data).toBe(42);
}, 500); // 測試必須在 <500ms 內完成
在 bun:test 中,超時會丟擲無法捕獲的異常,以強制測試停止執行並失敗。Bun 還會終止測試中生成的所有子行程,因此它們不會作為殭屍行程殘留。
除非使用此超時選項或 jest.setTimeout() 覆蓋,否則每個測試的預設超時時間為 5000ms(5 秒)。
重試和重複執行
test.retry
使用 retry 選項可在不穩定的測試失敗時自動重試。測試在指定的嘗試次數內成功即可通過。
import { expect, 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 次重複執行)。
import { expect, test } from "bun:test";
test(
"確保測試穩定",
() => {
expect(Math.random()).toBeLessThan(1);
},
{ repeats: 20 }, // 總計執行21次(1次初始 + 20次重複)
);
🧟 殭屍行程清理器
當測試超時時,Bun 會終止測試使用 Bun.spawn、Bun.spawnSync 或 node:child_process 生成的所有仍在執行的行程,並在控制台中記錄一條訊息。這可以防止超時測試結束後留下殭屍行程。
測試修飾符
test.skip
使用 test.skip 跳過單個測試。Bun 不會執行這些測試。
import { expect, test } from "bun:test";
test.skip("wat", () => {
// TODO: 修復此處
expect(0.1 + 0.2).toEqual(0.3);
});
test.todo
使用 test.todo 將測試標記為待辦事項。Bun 不會執行這些測試。
import { expect, test } from "bun:test";
test.todo("修復此處", () => {
myTestFunction();
});
要執行待辦測試並找出其中通過的測試,請使用 bun test --todo。當 myTestFunction() 正常工作後,Bun 會將上面的測試報告為失敗。
bun test --todo
math.test.ts:
✗ fix this
^ this test is marked as todo but passes. Remove `.todo` if tested behavior now works
0 pass
1 fail
使用此標誌時,失敗的待辦測試不會導致錯誤,但 Bun 會將通過的待辦測試標記為失敗,以便你移除待辦標記或修復測試。
test.only
要執行特定測試或測試套件,請使用 test.only() 或 describe.only()。
import { test, describe } from "bun:test";
test("測試 #1", () => {
// 不執行
});
test.only("測試 #2", () => {
// 執行
});
describe.only("僅此", () => {
test("測試 #3", () => {
// 執行
});
});
以下命令只執行測試 #2 和 #3。
bun test --only
test.if
要有條件地執行測試,請使用 test.if()。當條件為真值時,測試才會執行。對於僅應在特定架構或作業系統上執行的測試,可以使用它。
test.if(Math.random() > 0.5)("有一半機率執行", () => {
// ...
});
const macOS = process.platform === "darwin";
test.if(macOS)("僅在 macOS 執行", () => {
// 僅在 macOS 執行
});
test.skipIf
條件跳過測試,使用 test.skipIf() 或 describe.skipIf()。
const macOS = process.platform === "darwin";
test.skipIf(macOS)("僅在非 macOS 執行", () => {
// 在非 macOS 執行
});
test.todoIf
要改為將測試標記為待辦事項,請使用 test.todoIf() 或 describe.todoIf()。選擇 skipIf 還是 todoIf 表明了不同意圖:“對該目標無效”與“計劃實現但尚未實現”。
const macOS = process.platform === "darwin";
// TODO: 目前只在 Linux 上實現。
test.todoIf(macOS)("執行在 posix 系統上", () => {
// 在非 macOS 執行
});
test.failing
當你知道某個測試會失敗,但希望跟蹤它並在它開始通過時收到通知,請使用 test.failing()。這會反轉測試結果:
- 標記了
.failing()的失敗測試會通過 - 標記了
.failing()的通過測試會失敗,並顯示它現在已經通過、應當進行修復的訊息
// 因測試故意失敗,這裡會通過
test.failing("數學有問題", () => {
expect(0.1 + 0.2).toBe(0.3); // 由於浮點精度失敗
});
// 測試通過,但期望失敗,因此測試失敗並提示修復
test.failing("修復了 bug", () => {
expect(1 + 1).toBe(2); // 通過,但我們預期失敗
});
可以使用它跟蹤計劃稍後修復的已知 bug,或用於測試驅動開發。
describe 塊中的條件測試
條件修飾符 .if()、.skipIf() 和 .todoIf() 同樣適用於 describe 塊,會影響套件中的所有測試:
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.each 和 describe.each
使用 test.each 可針對多個數據集執行相同測試。每個測試用例都會執行一次。
const cases = [
[1, 2, 3],
[3, 4, 7],
];
test.each(cases)("%p + %p 應該是 %p", (a, b, expected) => {
expect(a + b).toBe(expected);
});
describe.each 建立一個引數化測試套件,每個測試用例執行一次:
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);
});
});
引數傳遞
Bun 如何向測試函式傳遞引數,取決於測試用例的結構:
- 如果表格行是陣列(例如
[1, 2, 3]),Bun 會將每個元素作為單獨的引數傳遞 - 如果行不是陣列(例如物件),Bun 會將其作為單個引數傳遞
// 陣列項作為單獨引數傳入
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);
});
格式化佔位符
使用以下格式說明符來格式化測試標題:
| 佔位符 | 描述 |
|---|---|
%p | pretty-format 美化輸出 |
%s | 字串 |
%d | 數字 |
%i | 整數 |
%f | 浮點數 |
%j | JSON |
%o | 物件 |
%# | 測試用例索引 |
%% | 單個百分號 (%) |
範例
// 基本佔位符
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() 驗證測試中至少呼叫過一次斷言:
test("非同步工作呼叫斷言", async () => {
expect.hasAssertions(); // 如果沒有斷言呼叫,測試失敗
const data = await fetchData();
expect(data).toBeDefined();
});
expect.hasAssertions() 在非同步測試中特別有用,可確保斷言確實執行。
expect.assertions(count)
使用 expect.assertions(count) 驗證測試中呼叫了指定數量的斷言:
test("恰好兩個斷言", () => {
expect.assertions(2); // 如果斷言不是精確2個則失敗
expect(1 + 1).toBe(2);
expect("hello").toContain("ell");
});
expect.assertions(count) 有助於確保所有斷言都執行,尤其是在包含多個程式碼路徑的複雜非同步程式碼中。
型別測試
Bun 包含用於測試 TypeScript 型別的 expectTypeOf,相容 Vitest。
expectTypeOf
expectTypeOf 函式提供型別級斷言,由 TypeScript 型別檢查器驗證。要測試型別:
- 使用
expectTypeOf編寫型別斷言 - 執行
bunx tsc --noEmit檢查型別是否正確
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 相容性;請參閱跟蹤 issue。
基本匹配器
| 狀態 | 匹配器 |
|---|---|
| ✅ | .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() |
最佳實踐
使用描述性的測試名稱
// 好的範例
test("應計算多個商品含稅總價", () => {
// 測試實現
});
// 避免
test("價格計算", () => {
// 測試實現
});
分組相關測試
describe("使用者認證", () => {
describe("有效憑證時", () => {
test("應傳回使用者資料", () => {
// 測試實現
});
test("應設定認證令牌", () => {
// 測試實現
});
});
describe("無效憑證時", () => {
test("應丟擲認證錯誤", () => {
// 測試實現
});
});
});
使用合適的匹配器
// 好的範例:使用具體匹配器
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);
測試錯誤情況
test("無效輸入應丟擲錯誤", () => {
expect(() => {
validateEmail("not-an-email");
}).toThrow("郵箱格式無效");
});
test("應處理非同步錯誤", async () => {
await expect(async () => {
await fetchUser("invalid-id");
}).rejects.toThrow("未找到使用者");
});
使用初始化和清理
import { beforeEach, afterEach, test } from "bun:test";
let testUser;
beforeEach(() => {
testUser = createTestUser();
});
afterEach(() => {
cleanupTestUser(testUser);
});
test("應更新使用者資料", () => {
// 在測試中使用 testUser
});