使用從內置 bun:test 模塊導入的類 Jest API 定義測試。長期來看,Bun 旨在實現完整的 Jest 兼容性;目前,支持有限的 expect 匹配器集合。
基本用法
定義一個簡單的測試:
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 { 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 選項在測試失敗時自動重試。如果測試在指定次數內成功,則測試通過。這對於可能間歇性失敗的不穩定測試很有用。
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 次重復)。
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 跳過單個測試。這些測試將不會運行。
import { expect, test } from "bun:test";
test.skip("wat", () => {
// TODO: 修復這個
expect(0.1 + 0.2).toEqual(0.3);
});test.todo
使用 test.todo 將測試標記為待辦。這些測試將不會運行。
import { expect, test } from "bun:test";
test.todo("fix this", () => {
myTestFunction();
});要運行待辦測試並查找任何通過的測試,請使用 bun test --todo。
bun test --todomy.test.ts:
✗ unimplemented feature
^ 此測試標記為待辦但通過了。刪除 `.todo` 或檢查測試是否正確。
0 pass
1 fail
1 expect() calls使用此標志時,失敗的待辦測試不會導致錯誤,但通過的待辦測試將標記為失敗,因此你可以刪除待辦標記或修復測試。
test.only
要運行特定測試或套件,請使用 test.only() 或 describe.only()。
import { test, describe } from "bun:test";
test("test #1", () => {
// 不運行
});
test.only("test #2", () => {
// 運行
});
describe.only("only", () => {
test("test #3", () => {
// 運行
});
});以下命令將僅執行測試 #2 和 #3。
bun test --only以下命令將僅執行測試 #1、#2 和 #3。
bun testtest.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("已修復的錯誤", () => {
expect(1 + 1).toBe(2); // 通過,但我們預期它會失敗
});這對於跟蹤你計劃稍後修復的已知錯誤,或實現測試驅動開發很有用。
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 上將整個套件標記為待辦
describe.todoIf(process.platform === "linux")("即將推出的 Linux 支持", () => {
test("功能 D", () => {
// 在 Linux 上標記為待辦
});
});參數化測試
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);
});
});參數傳遞
參數如何傳遞給測試函數取決於測試用例的結構:
- 如果表行是數組(如
[1, 2, 3]),則每個元素作為單獨的參數傳遞 - 如果行不是數組(如對象),則作為單個參數傳遞
// 數組項作為單獨參數傳遞
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 | String |
%d | Number |
%i | Integer |
%f | Floating point |
%j | JSON |
%o | Object |
%# | 測試用例的索引 |
%% | 單個百分號 (%) |
示例
// 基本說明符
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() 驗證在測試期間至少調用了一個斷言:
test("異步工作調用斷言", async () => {
expect.hasAssertions(); // 如果沒有調用斷言將失敗
const data = await fetchData();
expect(data).toBeDefined();
});這對於異步測試特別有用,可確保你的斷言實際運行。
expect.assertions(count)
使用 expect.assertions(count) 驗證在測試期間調用了特定數量的斷言:
test("正好兩個斷言", () => {
expect.assertions(2); // 如果沒有正好調用 2 個斷言將失敗
expect(1 + 1).toBe(2);
expect("hello").toContain("ell");
});這有助於確保所有斷言都運行,特別是在具有多個代碼路徑的復雜異步代碼中。
類型測試
Bun 包括 expectTypeOf 用於測試 TypeScript 類型,與 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 兼容性在路線圖上;在此處跟蹤進度。
基本匹配器
| 狀態 | 匹配器 |
|---|---|
| ✅ | .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
});