运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

生命周期钩子

了解如何在 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();
});