Jest 代码覆盖率 #

覆盖率概述 #

代码覆盖率是衡量测试质量的重要指标,表示代码被测试执行覆盖的程度。

text
┌─────────────────────────────────────────────────────────────┐
│                    覆盖率类型                                │
├─────────────────────────────────────────────────────────────┤
│  行覆盖率 (Line)       - 每行代码是否被执行                   │
│  分支覆盖率 (Branch)   - 每个分支是否被执行                   │
│  函数覆盖率 (Function) - 每个函数是否被调用                   │
│  语句覆盖率 (Statement)- 每条语句是否被执行                   │
└─────────────────────────────────────────────────────────────┘

基本使用 #

运行覆盖率 #

bash
# 命令行
jest --coverage

# npm script
npm test -- --coverage

# 简写
jest --collectCoverage

输出报告 #

text
----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |   90.5  |   85.2   |   92.1  |   90.5  |
  sum.js  |   100   |   100    |   100   |   100   |
  math.js |   85.7  |   75     |   88.9  |   85.7  | 12-15
----------|---------|----------|---------|---------|-------------------

配置覆盖率 #

jest.config.js #

javascript
module.exports = {
  // 收集覆盖率
  collectCoverage: true,

  // 覆盖率报告目录
  coverageDirectory: 'coverage',

  // 收集覆盖率的文件
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.js',
    '!**/node_modules/**',
  ],

  // 覆盖率报告格式
  coverageReporters: ['text', 'lcov', 'html'],

  // 覆盖率阈值
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

package.json #

json
{
  "jest": {
    "collectCoverage": true,
    "coverageDirectory": "coverage",
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}",
      "!src/**/*.d.ts"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

覆盖率报告格式 #

文本报告 #

javascript
coverageReporters: ['text', 'text-summary'],

输出到控制台。

HTML 报告 #

javascript
coverageReporters: ['html'],

生成可浏览的 HTML 报告:

text
coverage/
├── lcov-report/
│   ├── index.html
│   ├── src/
│   │   ├── index.html
│   │   ├── sum.js.html
│   │   └── math.js.html
│   └── ...

LCOV 报告 #

javascript
coverageReporters: ['lcov'],

生成 lcov.info 文件,用于 CI 工具集成。

JSON 报告 #

javascript
coverageReporters: ['json'],

生成 coverage-final.json 文件。

多种格式组合 #

javascript
coverageReporters: [
  'text',          // 控制台详细报告
  'text-summary',  // 控制台摘要
  'html',          // HTML 报告
  'lcov',          // LCOV 格式
  'json',          // JSON 格式
  'clover',        // Clover XML
  'cobertura',     // Cobertura XML
],

覆盖率阈值 #

全局阈值 #

javascript
coverageThreshold: {
  global: {
    branches: 80,
    functions: 80,
    lines: 80,
    statements: 80,
  },
},

文件级阈值 #

javascript
coverageThreshold: {
  global: {
    branches: 80,
    functions: 80,
    lines: 80,
    statements: 80,
  },
  './src/utils/': {
    branches: 100,
    functions: 100,
    lines: 100,
    statements: 100,
  },
  './src/components/': {
    branches: 70,
    functions: 70,
    lines: 70,
    statements: 70,
  },
},

负数阈值 #

javascript
coverageThreshold: {
  global: {
    branches: -10,  // 允许最多 10 个未覆盖分支
    functions: -5,  // 允许最多 5 个未覆盖函数
    lines: -20,     // 允许最多 20 行未覆盖
  },
},

收集覆盖率文件 #

包含文件 #

javascript
collectCoverageFrom: [
  'src/**/*.{js,jsx,ts,tsx}',  // 所有源文件
  'lib/**/*.js',                // lib 目录
],

排除文件 #

javascript
collectCoverageFrom: [
  'src/**/*.{js,jsx,ts,tsx}',
  '!src/**/*.d.ts',           // 排除类型声明
  '!src/index.js',            // 排除入口文件
  '!src/**/*.test.{js,ts}',   // 排除测试文件
  '!**/node_modules/**',      // 排除 node_modules
  '!**/__tests__/**',         // 排除测试目录
],

覆盖率忽略 #

忽略特定代码 #

javascript
// istanbul ignore next
function debugLog(message) {
  console.log(message);
}

// istanbul ignore else
if (process.env.NODE_ENV === 'production') {
  // 生产环境代码
} else {
  // 开发环境代码
}

/* istanbul ignore file */
// 整个文件忽略
export function helper() {}

忽略函数 #

javascript
function calculate(a, b) {
  /* istanbul ignore next */
  if (process.env.DEBUG) {
    console.log('Calculating...');
  }
  return a + b;
}

CI 集成 #

GitHub Actions #

yaml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'

      - run: npm ci
      - run: npm test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

GitLab CI #

yaml
test:
  script:
    - npm ci
    - npm test -- --coverage
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

Travis CI #

yaml
language: node_js
node_js:
  - 18

script:
  - npm test -- --coverage

after_success:
  - bash <(curl -s https://codecov.io/bash)

覆盖率服务 #

Codecov #

bash
# 安装
npm install --save-dev codecov

# 上传
npm run test -- --coverage
npx codecov

Coveralls #

bash
# 安装
npm install --save-dev coveralls

# 上传
npm run test -- --coverage
npx coveralls < coverage/lcov.info

SonarQube #

properties
# sonar-project.properties
sonar.projectKey=my-project
sonar.sources=src
sonar.tests=src
sonar.test.inclusions=**/*.test.js
sonar.javascript.lcov.reportPaths=coverage/lcov.info

覆盖率徽章 #

README 徽章 #

markdown
[![Coverage Status](https://codecov.io/gh/user/repo/branch/main/graph/badge.svg)](https://codecov.io/gh/user/repo)

[![Coverage](https://coveralls.io/repos/github/user/repo/badge.svg)](https://coveralls.io/github/user/repo)

实际示例 #

项目配置 #

javascript
// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  
  collectCoverage: false, // 默认不收集
  coverageDirectory: 'coverage',
  
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.tsx',
    '!src/reportWebVitals.ts',
    '!**/node_modules/**',
  ],
  
  coverageReporters: [
    'text',
    'text-summary',
    'html',
    'lcov',
  ],
  
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 70,
      lines: 70,
      statements: 70,
    },
  },
};

package.json scripts #

json
{
  "scripts": {
    "test": "jest",
    "test:coverage": "jest --coverage",
    "test:coverage:open": "npm run test:coverage && open coverage/lcov-report/index.html"
  }
}

查看覆盖率报告 #

bash
# 运行覆盖率
npm run test:coverage

# 打开 HTML 报告
open coverage/lcov-report/index.html

覆盖率解读 #

覆盖率指标含义 #

指标 描述 计算方式
Statements 语句覆盖率 执行语句数 / 总语句数
Branches 分支覆盖率 执行分支数 / 总分支数
Functions 函数覆盖率 调用函数数 / 总函数数
Lines 行覆盖率 执行行数 / 总行数

覆盖率不是越高越好 #

text
┌─────────────────────────────────────────────────────────────┐
│                    覆盖率误区                                │
├─────────────────────────────────────────────────────────────┤
│  ❌ 100% 覆盖率 = 没有 Bug                                   │
│  ❌ 覆盖率越高越好                                           │
│  ❌ 追求 100% 覆盖率                                         │
│                                                              │
│  ✅ 覆盖率是质量指标之一                                     │
│  ✅ 关注关键代码的覆盖                                       │
│  ✅ 平衡开发效率和覆盖率                                     │
└─────────────────────────────────────────────────────────────┘

合理的覆盖率目标 #

项目类型 建议覆盖率
核心库 90%+
业务应用 70-80%
UI 组件 60-70%
工具函数 90%+

提高覆盖率技巧 #

1. 测试边界情况 #

javascript
function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero');
  }
  return a / b;
}

// 测试
test('divide', () => {
  expect(divide(10, 2)).toBe(5);
  expect(() => divide(10, 0)).toThrow('Division by zero');
});

2. 测试错误路径 #

javascript
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error('User not found');
  }
  return response.json();
}

// 测试
test('fetchUser error', async () => {
  jest.spyOn(global, 'fetch').mockResolvedValue({ ok: false });
  await expect(fetchUser(1)).rejects.toThrow('User not found');
});

3. 测试条件分支 #

javascript
function getDiscount(user) {
  if (user.isPremium) {
    return 0.2;
  } else if (user.isMember) {
    return 0.1;
  }
  return 0;
}

// 测试所有分支
test('getDiscount', () => {
  expect(getDiscount({ isPremium: true })).toBe(0.2);
  expect(getDiscount({ isMember: true })).toBe(0.1);
  expect(getDiscount({})).toBe(0);
});

4. 使用参数化测试 #

javascript
test.each([
  [1, 2, 3],
  [2, 3, 5],
  [-1, 1, 0],
])('add(%i, %i) = %i', (a, b, expected) => {
  expect(add(a, b)).toBe(expected);
});

常见问题 #

覆盖率不准确 #

javascript
// 问题:覆盖率显示不正确
// 解决方案:清除缓存
jest --clearCache

覆盖率报告不完整 #

javascript
// 问题:某些文件没有出现在报告中
// 解决方案:检查 collectCoverageFrom 配置
collectCoverageFrom: [
  'src/**/*.{js,jsx,ts,tsx}',
  // 确保包含所有需要测试的文件
],

阈值检查失败 #

bash
# 问题:覆盖率低于阈值
# 解决方案:
# 1. 增加测试用例
# 2. 调整阈值
# 3. 忽略不需要测试的代码

最佳实践 #

1. 设置合理的阈值 #

javascript
coverageThreshold: {
  global: {
    branches: 70,  // 不要设置太高
    functions: 70,
    lines: 70,
    statements: 70,
  },
},

2. 定期检查覆盖率 #

bash
# CI 中强制覆盖率检查
npm test -- --coverage --ci

3. 关注关键代码 #

javascript
// 关键代码设置更高阈值
coverageThreshold: {
  './src/core/': {
    branches: 90,
    functions: 90,
    lines: 90,
  },
},

4. 使用覆盖率报告 #

bash
# 定期查看 HTML 报告
npm run test:coverage
open coverage/lcov-report/index.html

下一步 #

现在你已经掌握了 Jest 代码覆盖率,接下来学习 高级配置 深入了解 Jest 配置!

最后更新:2026-03-28