執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

生命週期鉤子

瞭解如何在 Bun 測試中使用 beforeAll、beforeEach、afterEach 和 afterAll 生命週期鉤子

測試執行器支援以下生命週期鉤子。使用它們載入測試裝置、模擬資料並設定測試環境。

鉤子描述
beforeAll在所有測試之前執行一次。
beforeEach在每個測試之前執行。
afterEach在每個測試之後執行。
afterAll在所有測試之後執行一次。
onTestFinished在單個測試完成後執行(在所有 afterEach 之後)。

每個測試的設定與拆卸

使用 beforeEach 和 afterEach 執行每個測試的設定與拆卸邏輯。

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

beforeEach(() => {
  console.log("執行測試。");
});

afterEach(() => {
  console.log("測試完成。");
});

// 測試...
test("範例測試", () => {
  // 此測試將在它之前執行 beforeEach
  // 並在它之後執行 afterEach
});

每個作用域的設定與拆卸

使用 beforeAll 和 afterAll 執行每個作用域的設定與拆卸邏輯。鉤子的定義位置決定其作用域。

限定在 describe 塊內

要將鉤子限定在特定的 describe 塊內:

test.ts
import { describe, beforeAll, afterAll, test } from "bun:test";

describe("測試組", () => {
  beforeAll(() => {
    // 此 describe 塊的設定
    console.log("設定測試組");
  });

  afterAll(() => {
    // 此 describe 塊的拆卸
    console.log("拆卸測試組");
  });

  test("測試 1", () => {
    // 測試實現
  });

  test("測試 2", () => {
    // 測試實現
  });
});

限定在測試檔案內

要將鉤子限定在整個測試檔案範圍:

test.ts
import { describe, beforeAll, afterAll, test } from "bun:test";

beforeAll(() => {
  // 整個檔案的設定
  console.log("設定測試檔案");
});

afterAll(() => {
  // 整個檔案的拆卸
  console.log("拆卸測試檔案");
});

describe("測試組", () => {
  test("測試 1", () => {
    // 測試實現
  });
});

onTestFinished

使用 onTestFinished 在單個測試完成後執行回撥。該回調會在所有 afterEach 鉤子之後執行。

test.ts
import { test, onTestFinished } from "bun:test";

test("測試後的清理", () => {
  onTestFinished(() => {
    // 在所有 afterEach 鉤子之後執行
    console.log("測試完成");
  });
});

並行測試不支援該鉤子;請改用 test.serial。

全域性設定與拆卸

要將鉤子限定於整個多檔案測試執行,請在單獨檔案中定義鉤子。

setup.ts
import { beforeAll, afterAll } from "bun:test";

beforeAll(() => {
  // 全域性設定
  console.log("全域性測試設定");
  // 初始化資料庫連線、啟動伺服器等
});

afterAll(() => {
  // 全域性拆卸
  console.log("全域性測試拆卸");
  // 關閉資料庫連線、停止伺服器等
});

然後使用 --preload 在任何測試檔案之前執行設定指令碼。

terminal
bun test --preload ./setup.ts

為避免每次執行測試時都輸入 --preload,請將其新增到 bunfig.toml 中:

bunfig.toml
[test]
preload = ["./setup.ts"]

實用範例

資料庫設定

database-setup.ts
import { beforeAll, afterAll, beforeEach, afterEach } from "bun:test";
import { createConnection, closeConnection, clearDatabase } from "./db";

let connection;

beforeAll(async () => {
  // 連線測試資料庫
  connection = await createConnection({
    host: "localhost",
    database: "test_db",
  });
});

afterAll(async () => {
  // 關閉資料庫連線
  await closeConnection(connection);
});

beforeEach(async () => {
  // 每個測試開始時清空資料庫
  await clearDatabase(connection);
});

API 伺服器設定

server-setup.ts
import { beforeAll, afterAll } from "bun:test";
import { startServer, stopServer } from "./server";

let server;

beforeAll(async () => {
  // 啟動測試伺服器
  server = await startServer({
    port: 3001,
    env: "test",
  });
});

afterAll(async () => {
  // 停止測試伺服器
  await stopServer(server);
});

模擬設定

mock-setup.ts
import { beforeEach, afterEach } from "bun:test";
import { mock } from "bun:test";

beforeEach(() => {
  // 設定通用模擬
  mock.module("./api-client", () => ({
    fetchUser: mock(() => Promise.resolve({ id: 1, name: "測試使用者" })),
    createUser: mock(() => Promise.resolve({ id: 2 })),
  }));
});

afterEach(() => {
  // Clear all mocks after each test
  mock.clearAllMocks();
});

非同步生命週期鉤子

所有生命週期鉤子都支援非同步函式:

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

beforeAll(async () => {
  // 非同步設定
  await new Promise(resolve => setTimeout(resolve, 100));
  console.log("非同步設定完成");
});

afterAll(async () => {
  // 非同步拆卸
  await new Promise(resolve => setTimeout(resolve, 100));
  console.log("非同步拆卸完成");
});

test("非同步測試", async () => {
  // 測試將等待 beforeAll 完成
  await expect(Promise.resolve("test")).resolves.toBe("test");
});

巢狀鉤子

你可以巢狀鉤子。它們按以下順序執行:

test.ts
import { describe, beforeAll, beforeEach, afterEach, afterAll, test } from "bun:test";

beforeAll(() => console.log("檔案 beforeAll"));
afterAll(() => console.log("檔案 afterAll"));

describe("外層 describe", () => {
  beforeAll(() => console.log("外層 beforeAll"));
  beforeEach(() => console.log("外層 beforeEach"));
  afterEach(() => console.log("外層 afterEach"));
  afterAll(() => console.log("外層 afterAll"));

  describe("內層 describe", () => {
    beforeAll(() => console.log("內層 beforeAll"));
    beforeEach(() => console.log("內層 beforeEach"));
    afterEach(() => console.log("內層 afterEach"));
    afterAll(() => console.log("內層 afterAll"));

    test("巢狀測試", () => {
      console.log("測試執行中");
    });
  });
});
Text
// 輸出順序:
// 檔案 beforeAll
// 外層 beforeAll
// 內層 beforeAll
// 外層 beforeEach
// 內層 beforeEach
// 測試執行中
// 內層 afterEach
// 外層 afterEach
// 內層 afterAll
// 外層 afterAll
// 檔案 afterAll

錯誤處理

如果 beforeAll 鉤子丟擲異常,測試執行器會跳過該鉤子作用域內的所有測試:

test.ts
import { beforeAll, test } from "bun:test";

beforeAll(() => {
  // 如果這裡丟擲,當前作用域的所有測試都將跳過
  throw new Error("設定失敗");
});

test("此測試將被跳過", () => {
  // 因為 beforeAll 失敗,此測試不會執行
});

要記錄設定失敗並仍然使測試套件失敗:

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

beforeAll(async () => {
  try {
    await setupDatabase();
  } catch (error) {
    console.error("資料庫設定失敗:", error);
    throw error; // 重新丟擲以使測試套件失敗
  }
});

最佳實踐

保持鉤子簡單

test.ts
// 好:簡單、專注的設定
beforeEach(() => {
  clearLocalStorage();
  resetMocks();
});

// 避免:鉤子中複雜邏輯
beforeEach(async () => {
  // 過於複雜的邏輯使測試難以除錯
  const data = await fetchComplexData();
  await processData(data);
  await setupMultipleServices(data);
});

使用合適的作用域

test.ts
// 好:檔案級別的共享資源設定
beforeAll(async () => {
  await startTestServer();
});

// 好:測試級別的測試特定狀態設定
beforeEach(() => {
  user = createTestUser();
});

清理資源

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

afterEach(() => {
  // 每個測試後的清理
  document.body.innerHTML = "";
  localStorage.clear();
});

afterAll(async () => {
  // 清理昂貴資源
  await closeDatabase();
  await stopServer();
});