覆盖率与生命周期
本教程共 34 篇 · 第 25 篇 · 更新于 2026-08-06
本节目标:
- 用
--coverage生成行/函数覆盖率报告,并理解阈值coverageThreshold、reporter(text/lcov)、忽略规则等配置。- 掌握五个生命周期钩子
beforeAll/beforeEach/afterEach/afterAll/onTestFinished,理解它们在不同作用域与嵌套下的执行顺序。- 理解 Bun 下 TypeScript 测试「零配置即可跑」的原因,以及
NODE_ENV=test、.env.test、--tsconfig-override等运行时行为。- 用
--watch进入开发时热重载测试,把前面所有能力组合进日常开发流。- 客观认识覆盖率指标的边界:它衡量「跑了哪些代码」,不衡量「测得好不好」。
25.1 覆盖率:看代码被测试覆盖了多少
bun test 内置覆盖率统计,无需接入 Istanbul / nyc 等外部工具。加一个 --coverage 即可在控制台看到报告:
bun test --coverage
输出形如:
-------------|---------|---------|-------------------
File | % Funcs | % Lines | Uncovered Line #s
-------------|---------|---------|-------------------
All files | 38.89 | 42.11 |
index-0.ts | 33.33 | 36.84 | 10-15,19-24
index.ts | 100.00 | 100.00 |
-------------|---------|---------|-------------------
- % Funcs:测试期间被调用过的函数占比。
- % Lines:测试期间被执行过的可执行行占比。
- Uncovered Line #s:从未被执行的行号,是补测试最直接的线索。
默认开启与文件排除
想让覆盖率默认开启,在 bunfig.toml 里配置:
[test]
coverage = true
默认情况下,覆盖率会包含测试文件本身(匹配 *.test.ts、*.spec.js 等模式),同时排除 node_modules 与通过非 JS/TS loader(如 .css、.txt)加载的文件。若你想把测试文件排除在统计之外,把它设为 true:
[test]
coverageSkipTestFiles = true # 默认 false
阈值:覆盖不到就失败
CI 里常用「覆盖率不达标就让流程失败」来守住质量底线。coverageThreshold 支持单个数值(同时作用于行、函数、语句),也支持分维度配置:
[test]
# 要求 90% 覆盖率(行/函数/语句统一)
coverageThreshold = 0.9
[test]
# 分维度设定不同阈值
coverageThreshold = { lines = 0.9, functions = 0.9, statements = 0.9 }
只要设了阈值,Bun 会自动开启 fail_on_low_coverage:覆盖率低于阈值时 bun test 以非零状态码退出,CI 随之失败。
Reporter 与导出
默认只打印 text 到控制台。要导出给 Codecov、Coveralls 或编辑器用的 LCOV 报告,加 lcov:
[test]
coverageReporter = ["text", "lcov"] # 默认 ["text"]
coverageDir = "coverage" # 默认 "coverage"
# 等价命令行
bun test --coverage --coverage-reporter=lcov
LCOV 的 lcov.info 可直接喂给 GitHub Actions 的 codecov-action:
- run: bun test --coverage --coverage-reporter=lcov
- uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
忽略特定路径
用 coveragePathIgnorePatterns 排除不想统计的文件(如配置文件、构建产物、第三方代码)。它接受 glob,类似 Jest 的 collectCoverageFrom 忽略规则:
[test]
coveragePathIgnorePatterns = [
"**/*.spec.ts",
"src/utils/**",
"*.config.js",
"vendor/**",
]
Note覆盖率只统计「被加载过的文件」。如果某个模块从未被任何测试
import,它不会出现在报告里,自然也谈不上被覆盖。看到覆盖率「虚高」时,先确认测试是否真的导入了要测的代码。
25.2 生命周期钩子
测试常常需要在运行前后做统一准备与清理:连数据库、起服务、造测试数据、复位全局状态。Bun 提供五个钩子:
| 钩子 | 作用 |
|---|---|
beforeAll | 在本作用域内所有测试前运行一次 |
beforeEach | 在每个测试前运行 |
afterEach | 在每个测试后运行 |
afterAll | 在本作用域内所有测试后运行一次 |
onTestFinished | 单个测试结束后运行(晚于所有 afterEach) |
按作用域分档
钩子「作用到哪」,取决于它定义在哪一层:
- 文件级:直接写在测试文件顶层,作用于该文件所有测试。
- describe 级:写在某个
describe块内,只作用于该块。 - 全局级:写在独立文件里,通过
--preload加载,作用于整个测试运行。
import { describe, beforeAll, afterAll, test } from "bun:test";
beforeAll(() => {
console.log("Setting up test file");
});
afterAll(() => {
console.log("Tearing down test file");
});
describe("test group", () => {
test("test 1", () => {});
});
数据库场景是 beforeAll / afterAll 的典型用法——连接与断开只需一次:
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);
});
嵌套顺序
describe 可以嵌套,钩子按「由外到内 setup、由内到外 teardown」的顺序执行:
import { describe, beforeAll, beforeEach, afterEach, afterAll, test } from "bun:test";
beforeAll(() => console.log("File beforeAll"));
afterAll(() => console.log("File afterAll"));
describe("outer describe", () => {
beforeAll(() => console.log("Outer beforeAll"));
beforeEach(() => console.log("Outer beforeEach"));
afterEach(() => console.log("Outer afterEach"));
afterAll(() => console.log("Outer afterAll"));
describe("inner describe", () => {
beforeAll(() => console.log("Inner beforeAll"));
beforeEach(() => console.log("Inner beforeEach"));
afterEach(() => console.log("Inner afterEach"));
afterAll(() => console.log("Inner afterAll"));
test("nested test", () => {
console.log("Test running");
});
});
});
打印顺序为:
File beforeAll
Outer beforeAll
Inner beforeAll
Outer beforeEach
Inner beforeEach
Test running
Inner afterEach
Outer afterEach
Inner afterAll
Outer afterAll
File afterAll
onTestFinished 与错误处理
onTestFinished 在每个测试(及它的 afterEach)都结束后触发,适合做「无论测试成败都要做的清理」。它与并发测试不兼容,需要时用 test.serial。
如果 beforeAll 抛错,其作用域内所有测试都会被跳过——这是预期行为,因为前置环境不可用。需要既记日志又让套件失败时,可 try/catch 后重新 throw:
beforeAll(async () => {
try {
await setupDatabase();
} catch (error) {
console.error("Database setup failed:", error);
throw error; // 重新抛出,使套件失败
}
});
Tip全局性的准备/清理(起服务、连库)放
--preload文件里,配合beforeAll/afterAll;而「每个测试都要复位的状态」放beforeEach/afterEach。作用域选对了,测试既快又干净。
25.3 TypeScript 测试的零配置体验
前面所有示例都是 .ts / .tsx,却从不需要任何 ts-jest、babel、swc 之类的转译链——因为 Bun 运行时内置了对 TypeScript 与 JSX 的转译,bun test 直接执行测试文件。
- 类型只是被剥离,不参与运行:Bun 在运行前去掉类型注解,类型错误不会让测试跑不起来(但类型检查本身也不在测试过程中发生)。要单独做类型检查,用
bunx tsc --noEmit。 - 用
bun:test显式导入:官方示例都采用import { test, expect } from "bun:test",这样在 TypeScript 里能拿到精确的类型提示。安装@types/bun后,这些导出与测试全局变量都会被正确标注类型。 - 自定义 tsconfig:需要针对测试用不同的编译选项时,可传
--tsconfig-override ./test-tsconfig.json。
NODE_ENV 与 .env.test
bun test 默认把 process.env.NODE_ENV 设为 "test"(除非环境或 .env 里已经设过),大多数测试框架都这么做:
import { test, expect } from "bun:test";
test("NODE_ENV is set to test", () => {
expect(process.env.NODE_ENV).toBe("test");
});
测试专用的环境变量可以放进 .env.test,bun test 会自动加载;也可以在运行命令里显式指定:
bun test --env-file .env.test
Note需要换时区时,默认测试运行在 UTC(
Etc/UTC)。用TZ=America/New_York bun test即可让所有日期相关断言按纽约时区执行,结果在不同机器间保持一致。
25.4 —watch:开发时热重载测试
写测试时最舒服的模式是「改一下、自动重跑」。bun test --watch 会监听文件变化,一旦有文件保存就重新执行相关测试,而且因为是 Bun,重启极快:
bun test --watch
如果想更激进地保留运行期间的状态,可用 --hot(watch 的变体,跨次运行之间尽量保留状态)。对绝大多数测试,--watch 更合适——它在每次运行间提供更好的隔离,避免上一次测试留下的全局状态污染下一次。
Tip把覆盖率也常开时,
watch模式下每轮重跑都会刷新报告,适合在本地边写边看「还差哪几行没覆盖」。CI 里则通常只用一次性bun test --coverage --coverage-reporter=lcov。
25.5 把能力串起来:典型 bunfig.toml
一个组合了前面所有主题的 bunfig.toml 长这样:
[install]
registry = "https://registry.npmjs.org/"
exact = true
[test]
# 发现与 preload
root = "src"
preload = ["./test-setup.ts"]
pathIgnorePatterns = ["vendor/**", "submodules/**"]
# 覆盖率
coverage = true
coverageReporter = ["text", "lcov"]
coverageDir = "./coverage"
coverageThreshold = { lines = 0.85, functions = 0.90, statements = 0.80 }
coverageSkipTestFiles = true
coveragePathIgnorePatterns = ["**/*.spec.ts", "src/utils/**", "*.config.js"]
# Reporter
[test.reporter]
junit = "./reports/junit.xml"
其中 test-setup.ts 可以同时承担「注册 happy-dom、引入 jest-dom、注册全局 mock、定义 beforeAll/afterAll 与 afterEach 清理」等职责,是整条测试链路的统一入口。
25.6 小结与误区
- 覆盖率:
-coverage看报告,coverageThreshold守底线,lcov喂 CI;它只统计被加载的文件。 - 生命周期:
beforeAll/beforeEach/afterEach/afterAll/onTestFinished按作用域(文件 / describe / 全局)与嵌套(外到内 setup、内到外 teardown)执行。 - TypeScript 测试零配置即可跑;
NODE_ENV默认test,.env.test自动加载,@types/bun提供类型,--tsconfig-override可换配置。 --watch实现改完即重跑的开发流;--hot更激进保留状态。
Warning不要把覆盖率当唯一质量指标。100% 覆盖率也可能全是「只调用不断言」的空壳测试。覆盖率回答「跑了哪段代码」,回答不了「业务逻辑对不对」——后者还得靠有实质断言的用例来保证。