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 的开发时自动重载功能。