Skip to content

Bun 测试运行器

Bun 内置了快速的测试运行器,兼容 Jest 语法,支持 TypeScript,无需额外配置。本章介绍 Bun 测试的使用方法。

快速开始

创建测试文件

typescript
// math.test.ts
import { expect, test } from "bun:test";

function add(a: number, b: number): number {
  return a + b;
}

test("加法测试", () => {
  expect(add(2, 3)).toBe(5);
});

运行测试

bash
# 运行所有测试
bun test

# 运行特定文件
bun test math.test.ts

# 运行匹配的测试文件
bun test --pattern "*.spec.ts"

# 监听模式
bun test --watch

测试语法

test 函数

typescript
import { test, expect } from "bun:test";

// 基本测试
test("基本测试", () => {
  expect(1 + 1).toBe(2);
});

// 异步测试
test("异步测试", async () => {
  const result = await Promise.resolve(42);
  expect(result).toBe(42);
});

// 跳过测试
test.skip("跳过的测试", () => {
  // 不会执行
});

// 只运行这个测试
test.only("只运行这个", () => {
  expect(true).toBe(true);
});

// 待实现的测试
test.todo("待实现的功能");

// 带超时的测试
test("超时测试", async () => {
  await Bun.sleep(100);
}, { timeout: 5000 });

describe 分组

typescript
import { describe, test, expect } from "bun:test";

describe("数学运算", () => {
  test("加法", () => {
    expect(1 + 1).toBe(2);
  });
  
  test("减法", () => {
    expect(5 - 3).toBe(2);
  });
  
  describe("高级运算", () => {
    test("乘法", () => {
      expect(2 * 3).toBe(6);
    });
    
    test("除法", () => {
      expect(6 / 2).toBe(3);
    });
  });
});

生命周期钩子

typescript
import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from "bun:test";

describe("生命周期测试", () => {
  let db: Database;
  
  beforeAll(async () => {
    // 所有测试前执行一次
    db = await Database.connect();
    console.log("数据库已连接");
  });
  
  afterAll(async () => {
    // 所有测试后执行一次
    await db.close();
    console.log("数据库已关闭");
  });
  
  beforeEach(() => {
    // 每个测试前执行
    console.log("准备测试数据");
  });
  
  afterEach(() => {
    // 每个测试后执行
    console.log("清理测试数据");
  });
  
  test("测试 1", () => {
    expect(db.isConnected()).toBe(true);
  });
  
  test("测试 2", () => {
    expect(db.isConnected()).toBe(true);
  });
});

断言方法

基本断言

typescript
import { expect, test } from "bun:test";

test("基本断言", () => {
  // 相等
  expect(1 + 1).toBe(2);
  expect({ a: 1 }).toEqual({ a: 1 });
  
  // 不相等
  expect(1).not.toBe(2);
  
  // 真值/假值
  expect(true).toBeTruthy();
  expect(false).toBeFalsy();
  expect(null).toBeNull();
  expect(undefined).toBeUndefined();
  expect("hello").toBeDefined();
  
  // 类型检查
  expect(typeof "hello").toBe("string");
  expect([]).toBeInstanceOf(Array);
});

数值断言

typescript
test("数值断言", () => {
  expect(10).toBeGreaterThan(5);
  expect(10).toBeGreaterThanOrEqual(10);
  expect(5).toBeLessThan(10);
  expect(5).toBeLessThanOrEqual(5);
  
  // 浮点数比较
  expect(0.1 + 0.2).toBeCloseTo(0.3);
  expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // 5 位精度
  
  // NaN 检查
  expect(NaN).toBeNaN();
});

字符串断言

typescript
test("字符串断言", () => {
  expect("hello world").toContain("world");
  expect("hello world").toMatch(/hello/);
  expect("hello world").toStartWith("hello");
  expect("hello world").toEndWith("world");
  expect("hello").toHaveLength(5);
});

数组断言

typescript
test("数组断言", () => {
  const arr = [1, 2, 3];
  
  expect(arr).toContain(2);
  expect(arr).toHaveLength(3);
  expect(arr).toEqual([1, 2, 3]);
  
  // 数组包含对象
  const users = [{ id: 1, name: "张三" }];
  expect(users).toContainEqual({ id: 1, name: "张三" });
});

对象断言

typescript
test("对象断言", () => {
  const obj = { a: 1, b: 2, c: { d: 3 } };
  
  expect(obj).toEqual({ a: 1, b: 2, c: { d: 3 } });
  expect(obj).toMatchObject({ a: 1, b: 2 });
  expect(obj).toHaveProperty("a");
  expect(obj).toHaveProperty("a", 1);
  expect(obj).toHaveProperty("c.d", 3);
});

异常断言

typescript
test("异常断言", () => {
  function throwError() {
    throw new Error("出错了");
  }
  
  expect(throwError).toThrow();
  expect(throwError).toThrow("出错了");
  expect(throwError).toThrow(/出错/);
  expect(throwError).toThrow(Error);
});

test("异步异常", async () => {
  async function asyncThrow() {
    throw new Error("异步错误");
  }
  
  expect(asyncThrow()).rejects.toThrow("异步错误");
});

Mock 功能

Mock 函数

typescript
import { test, expect, mock } from "bun:test";

test("mock 函数", () => {
  // 创建 mock 函数
  const fn = mock(() => 42);
  
  // 调用
  expect(fn()).toBe(42);
  expect(fn(1, 2)).toBe(42);
  
  // 验证调用
  expect(fn).toHaveBeenCalled();
  expect(fn).toHaveBeenCalledTimes(2);
  expect(fn).toHaveBeenCalledWith(1, 2);
});

test("mock 实现", () => {
  const fn = mock();
  
  // 设置返回值
  fn.mockReturnValue(100);
  expect(fn()).toBe(100);
  
  // 设置一次性返回值
  fn.mockReturnValueOnce(1);
  fn.mockReturnValueOnce(2);
  expect(fn()).toBe(1);
  expect(fn()).toBe(2);
  expect(fn()).toBe(100);
  
  // 设置实现
  fn.mockImplementation((a, b) => a + b);
  expect(fn(2, 3)).toBe(5);
});

Spy 函数

typescript
import { test, expect, spyOn } from "bun:test";

test("spy 函数", () => {
  const obj = {
    greet(name: string) {
      return `Hello, ${name}!`;
    },
  };
  
  // 监听方法
  const spy = spyOn(obj, "greet");
  
  // 调用原方法
  expect(obj.greet("Bun")).toBe("Hello, Bun!");
  
  // 验证调用
  expect(spy).toHaveBeenCalled();
  expect(spy).toHaveBeenCalledWith("Bun");
  
  // 恢复原方法
  spy.mockRestore();
});

Mock 模块

typescript
import { test, expect, mock } from "bun:test";

// mock 整个模块
mock.module("./api", () => ({
  fetchUsers: mock(() => Promise.resolve([{ id: 1, name: "张三" }])),
  fetchPosts: mock(() => Promise.resolve([])),
}));

// 现在导入的是 mock 版本
import { fetchUsers, fetchPosts } from "./api";

test("mock 模块", async () => {
  const users = await fetchUsers();
  expect(users).toHaveLength(1);
  expect(users[0].name).toBe("张三");
});

快照测试

基本快照

typescript
import { test, expect } from "bun:test";

test("快照测试", () => {
  const user = {
    id: 1,
    name: "张三",
    email: "zhangsan@example.com",
    createdAt: new Date("2024-01-01"),
  };
  
  expect(user).toMatchSnapshot();
});

更新快照

bash
# 更新所有快照
bun test --update-snapshots

# 简写
bun test -u

内联快照

typescript
test("内联快照", () => {
  const result = { a: 1, b: 2 };
  
  expect(result).toMatchInlineSnapshot(`
    {
      "a": 1,
      "b": 2,
    }
  `);
});

代码覆盖率

启用覆盖率

bash
# 运行测试并生成覆盖率报告
bun test --coverage

覆盖率输出

----------|---------|----------|---------|---------|
File      | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
All files |   85.71 |    75.00 |   66.67 |   85.71 |
 math.ts  |  100.00 |   100.00 |  100.00 |  100.00 |
 utils.ts |   75.00 |    50.00 |   50.00 |   75.00 |
----------|---------|----------|---------|---------|

覆盖率配置

toml
# bunfig.toml
[test]
coverage = true
coverageThreshold = 80

# 忽略的文件
coverageIgnorePatterns = [
  "node_modules",
  "test",
  "*.config.*",
]

测试配置

bunfig.toml 配置

toml
[test]
# 测试文件匹配
testMatch = ["**/*.test.ts", "**/*.spec.ts"]

# 超时时间(毫秒)
timeout = 5000

# 预加载脚本
preload = ["./test/setup.ts"]

# 启用覆盖率
coverage = true

# 覆盖率阈值
coverageThreshold = 80

测试设置文件

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

// 全局设置
beforeAll(() => {
  console.log("开始测试...");
});

afterAll(() => {
  console.log("测试完成!");
});

// 全局 mock
global.fetch = mock(() => 
  Promise.resolve(new Response("mocked"))
);

异步测试

Promise 测试

typescript
test("Promise 测试", async () => {
  const promise = Promise.resolve(42);
  await expect(promise).resolves.toBe(42);
  
  const rejected = Promise.reject(new Error("失败"));
  await expect(rejected).rejects.toThrow("失败");
});

定时器测试

typescript
import { test, expect, setSystemTime } from "bun:test";

test("定时器测试", () => {
  // 模拟系统时间
  setSystemTime(new Date("2024-01-01T00:00:00Z"));
  
  expect(new Date().toISOString()).toBe("2024-01-01T00:00:00.000Z");
  
  // 恢复真实时间
  setSystemTime();
});

实际测试示例

API 服务测试

typescript
// api.test.ts
import { describe, test, expect, beforeAll, afterAll } from "bun:test";

describe("API 测试", () => {
  let server: ReturnType<typeof Bun.serve>;
  let baseUrl: string;
  
  beforeAll(() => {
    server = Bun.serve({
      port: 0, // 随机端口
      fetch(request) {
        const url = new URL(request.url);
        
        if (url.pathname === "/api/users") {
          return Response.json([{ id: 1, name: "张三" }]);
        }
        
        return new Response("Not Found", { status: 404 });
      },
    });
    
    baseUrl = `http://localhost:${server.port}`;
  });
  
  afterAll(() => {
    server.stop();
  });
  
  test("获取用户列表", async () => {
    const response = await fetch(`${baseUrl}/api/users`);
    
    expect(response.ok).toBe(true);
    
    const users = await response.json();
    expect(users).toHaveLength(1);
    expect(users[0].name).toBe("张三");
  });
  
  test("404 响应", async () => {
    const response = await fetch(`${baseUrl}/not-found`);
    expect(response.status).toBe(404);
  });
});

数据库测试

typescript
// db.test.ts
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import { Database } from "bun:sqlite";

describe("数据库测试", () => {
  let db: Database;
  
  beforeEach(() => {
    db = new Database(":memory:");
    db.run(`
      CREATE TABLE users (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        email TEXT UNIQUE
      )
    `);
  });
  
  afterEach(() => {
    db.close();
  });
  
  test("插入用户", () => {
    const stmt = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
    stmt.run("张三", "zhangsan@example.com");
    
    const user = db.query("SELECT * FROM users WHERE name = ?").get("张三");
    expect(user).toMatchObject({ name: "张三", email: "zhangsan@example.com" });
  });
  
  test("查询用户", () => {
    db.run("INSERT INTO users (name, email) VALUES ('张三', 'a@b.com')");
    db.run("INSERT INTO users (name, email) VALUES ('李四', 'c@d.com')");
    
    const users = db.query("SELECT * FROM users").all();
    expect(users).toHaveLength(2);
  });
});

小结

本章介绍了:

  • ✅ 测试文件创建和运行
  • ✅ test、describe 和生命周期钩子
  • ✅ 各种断言方法
  • ✅ Mock 和 Spy 功能
  • ✅ 快照测试
  • ✅ 代码覆盖率
  • ✅ 实际测试示例

下一步

继续阅读 热重载 了解 Bun 的开发时自动重载功能。

本站内容仅供学习和研究使用。