Jest Coverage Metrics 详解
Jest 测试覆盖率使用 Istanbul (nyc) 作为底层引擎,提供4种不同维度的覆盖率指标:
| 口径 | 英文 | 含义 | 计算公式 |
|---|---|---|---|
| 语句覆盖率 | Statements | 代码语句被执行的比例 | 已执行语句数 / 总语句数 |
| 分支覆盖率 | Branches | 条件分支被覆盖的比例 | 已覆盖分支数 / 总分支数 |
| 函数覆盖率 | Functions | 函数被调用的比例 | 已调用函数数 / 总函数数 |
| 行覆盖率 | Lines | 代码行被执行的比例 | 已执行行数 / 总行数 |
衡量代码中的语句是否被执行。一行代码可能包含多个语句。
// 这是 1 行,但包含 2 个语句
let x = 1; let y = 2;
// 这是 1 行 1 语句
const sum = x + y;
衡量条件语句的每个分支是否都被测试。这是最重要的指标,因为它确保了逻辑的完整性。
// 有 2 个分支:if 为真 和 if 为假
if (user.isAdmin) {
grantAccess(); // 分支 1
} else {
denyAccess(); // 分支 2
}
// 三元运算符也有 2 个分支
const status = isActive ? 'active' : 'inactive';
// switch 语句每个 case 都是一个分支
switch (type) {
case 'A': ... // 分支 1
case 'B': ... // 分支 2
default: ... // 分支 3
}
衡量函数是否被调用过。不关心函数内部逻辑是否完整覆盖。
// 只要这个函数被调用一次,函数覆盖率就是 100%
function calculateTotal(items) {
if (items.length === 0) return 0; // 分支可能未覆盖
return items.reduce((sum, i) => sum + i.price, 0);
}
衡量代码行是否被执行。与语句覆盖率类似,但以行为单位。
backend/jest.config.jscoverageThreshold: {
global: {
branches: 60,
functions: 60,
lines: 60,
statements: 60
}
}
frontend/web_app/jest.config.jscoverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70
}
}
# 后端覆盖率
cd backend && npm run test:coverage
# 前端覆盖率
cd frontend/web_app && npm run test:coverage
# 根目录快捷命令(后端+前端)
npm run test:coverage
运行覆盖率测试后,会在 coverage/ 目录生成 HTML 报告:
# 打开后端覆盖率报告
open backend/coverage/lcov-report/index.html
# 打开前端覆盖率报告
open frontend/web_app/coverage/lcov-report/index.html
-----------------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
-----------------------------|---------|----------|---------|---------|
All files | 85.23 | 72.15 | 91.45 | 85.67 |
services/ | 92.34 | 84.21 | 95.00 | 92.56 |
auditLogger.js | 97.05 | 76.47 | 100.00 | 100.00 |
checkinService.js | 100.00 | 87.69 | 100.00 | 100.00 |
invoiceService.js | 100.00 | 98.21 | 100.00 | 100.00 |
-----------------------------|---------|----------|---------|---------|
即使语句/函数/行覆盖率都是 100%,分支覆盖率可能只有 50%。
这意味着你的代码逻辑只测试了一半的情况,另一半可能隐藏着 bug。
// 示例:语句覆盖 100%,分支覆盖 50%
function getDiscount(customer) {
if (customer.isVip) {
return 0.2; // ✅ 被测试了
}
return 0; // ❌ 从未被测试!
}
// 只写了这个测试
test('VIP gets 20% discount', () => {
expect(getDiscount({ isVip: true })).toBe(0.2);
});
项目配置了 .github/workflows/ci-test.yml 在 PR 时自动检查覆盖率:
项目提供统一测试运行器,可一次性运行所有子项目的测试,覆盖 465+ 个测试文件:
| 项目 | 类别 | 文件数 | 框架 |
|---|---|---|---|
| Backend | 单元/API/集成测试 | 252+ | Jest |
| Frontend | 单元 + E2E 测试 | 173 | Jest + Playwright |
| Landing | 单元 + E2E 测试 | 3 | Vitest + Playwright |
| Guest App | 单元 + E2E 测试 | 23 | Flutter Test |
| Employee App | 单元 + E2E 测试 | 14 | Flutter Test |
# 日常自检(推荐)- 后端 + 前端 + Landing
npm run test:all
# 快速测试(提交前)- 仅单元测试
npm run test:all:quick
# 完整测试(发布前)- 含 Flutter 移动端
npm run test:all:full
# CI 模式 - 含覆盖率报告
npm run test:all:ci
# 按测试类型
npm run test:all:unit # 仅所有单元测试
npm run test:all:integration # 仅所有集成测试
npm run test:all:e2e # 仅所有 E2E 测试
直接调用脚本 ./scripts/test-all.sh [options] 支持更多选项:
| 参数 | 说明 |
|---|---|
--backend | 仅后端测试 |
--frontend | 仅前端测试 |
--landing | 仅 Landing Page 测试 |
--flutter | 仅 Flutter 移动端测试 |
--guest-app | 仅 Guest App 测试 |
--employee-app | 仅 Employee App 测试 |
--unit | 仅单元测试 |
--integration | 仅集成测试 |
--e2e | 仅 E2E 测试 |
--quick | 快速模式(跳过慢测试) |
--ci | CI 模式(含覆盖率报告) |
--full | 完整模式(含移动端) |
--verbose | 详细输出 |
--report | 生成汇总报告(默认开启) |
test-reports/
├── backend/ # 后端测试报告
│ ├── unit/
│ ├── integration/
│ ├── api/
│ └── sync/
├── frontend/ # 前端测试报告
│ ├── unit/
│ └── e2e/
├── landing/ # Landing Page 测试报告
├── flutter/ # 移动端测试报告
│ ├── guest-app/
│ └── employee-app/
└── summary/ # 汇总报告
└── latest.md # 最新汇总报告(符号链接)
cat test-reports/summary/latest.md
Guest App 的 Flutter E2E 测试也可以单独运行:
# 运行所有 Flutter E2E 测试
npm run test:flutter
# 按模块运行
npm run test:flutter:auth # 认证流程
npm run test:flutter:booking # 预约流程
npm run test:flutter:cart # 购物车流程
npm run test:flutter:appointments # 预约列表
npm run test:flutter:account # 账户/设置
npm run test:flutter:config # AppConfig 动态配置
Flutter 集成测试需要连接 iOS Simulator 或 Android Emulator。
如果检测到多个设备,需要指定设备:flutter test integration_test/ -d <device_id>