Chapter 06

bun test:测试框架

Jest 兼容 API + Zig 原生实现,启动毫秒级,跑百倍于 jest 的速度——测试驱动开发变得几乎无延迟。

6.1 最小测试

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

test("1 + 1 = 2", () => {
  expect(1 + 1).toBe(2);
});
$ bun test
bun test v1.2.x

sum.test.ts:
✓ 1 + 1 = 2 [0.5ms]

 1 pass
 0 fail
Ran 1 test across 1 file. [12ms]

默认匹配 *.test.{js,ts,tsx} / *.spec.{...} / __tests__/ 下的文件。

6.2 describe / it / expect

import { describe, it, expect, beforeEach, afterAll } from "bun:test";

describe("User", () => {
  let user;

  beforeEach(() => { user = { name: "Alice", age: 30 }; });
  afterAll(() => { /* 全组跑完清理 */ });

  it("has name", () => expect(user.name).toBe("Alice"));
  it("adult",     () => expect(user.age).toBeGreaterThanOrEqual(18));
});

生命周期钩子齐全:beforeAll/afterAll/beforeEach/afterEach

6.3 expect 匹配器

与 Jest API 一致(部分常用):

expect(x).toBe(1);                  // === 严格相等
expect(obj).toEqual({ a: 1 });        // 深度相等
expect(s).toMatch(/^hello/);
expect(arr).toContain("bun");
expect(fn).toThrow(TypeError);
expect(p).resolves.toBe(42);         // Promise 解决值
expect(p).rejects.toThrow();

expect(obj).toHaveProperty("x.y", 1);  // 嵌套属性
expect(3.14).toBeCloseTo(3.1, 1);
expect(mockFn).toHaveBeenCalledWith("x", 42);

6.4 异步测试

test("fetch user", async () => {
  const res = await fetch("https://api.example.com/user/1");
  expect(res.status).toBe(200);
  const json = await res.json();
  expect(json.name).toBeDefined();
});

6.5 Mock 与 Spy

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

const fn = mock((x: number) => x * 2);
fn(3);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith(3);
expect(fn.mock.results[0].value).toBe(6);

// Spy:替换对象方法,保留原实现或覆盖
const obj = { greet: (n: string) => `hi ${n}` };
const s = spyOn(obj, "greet");
obj.greet("Alice");
expect(s).toHaveBeenCalledWith("Alice");

// 模块 mock
import { mock } from "bun:test";
mock.module("./user", () => ({
  getUser: () => ({ id: 1, name: "stub" }),
}));

6.6 Snapshot 测试

test("render", () => {
  const tree = renderComponent();
  expect(tree).toMatchSnapshot();
});

首次跑生成 __snapshots__/foo.test.ts.snap,之后每次跑对比。手动更新:

$ bun test --update-snapshots

6.7 过滤 / 聚焦

$ bun test user              # 只跑路径含 "user" 的
$ bun test --only            # 只跑 test.only/describe.only 标记的
$ bun test -t "登录失败"     # 按用例名正则匹配

代码里:

test.only("focus", () => {});        // 配 --only 专跑这个
test.skip("skip", () => {});        // 跳过
test.todo("未来");                     // 待写
test.each([
  [1, 1, 2],
  [2, 3, 5],
])("%i + %i = %i", (a, b, expected) => {
  expect(a + b).toBe(expected);
});

6.8 覆盖率 coverage

$ bun test --coverage

 File       | % Funcs | % Lines | Uncovered
------------|---------|---------|----------
 sum.ts     |  100.00 |   80.00 | 5-7
 user.ts    |   50.00 |   62.50 | 10-12,17

配置放 bunfig.toml:

[test]
coverage = true
coverageThreshold = { line = 0.8, function = 0.9 }
coverageReporter = ["text", "lcov"]
coverageDir = "coverage"

6.9 与 Jest / Vitest 的对照

功能JestVitestbun test
API(describe/it/expect)✅ Jest 兼容
启动时间~1-3s~300ms< 30ms
原生 TS/JSX需 ts-jest/SWC
并行执行workersworkers并行
模块 mockjest.mockvi.mockmock.module
覆盖率istanbulc8/istanbul内置 (llvm)

6.10 preload 脚本

在每个测试文件跑之前先执行某个初始化:

# bunfig.toml
[test]
preload = ["./test-setup.ts"]
// test-setup.ts —— 设全局 mock、建 DB、注册 custom matchers
import { beforeAll } from "bun:test";
beforeAll(() => {
  process.env.NODE_ENV = "test";
});

6.11 DOM 测试:happy-dom

测试前端组件时需要 DOM 模拟——Bun 与 happy-dom 结合好,preload 里装上:

// happydom.ts
import { GlobalRegistrator } from "@happy-dom/global-registrator";
GlobalRegistrator.register();
[test]
preload = ["./happydom.ts"]

6.12 小结