Skip to content

测试覆盖率

Vitest 通过 v8 支持原生代码覆盖率,通过 istanbul 支持检测代码覆盖率。

测试覆盖率提供者

v8istanbul 的支持都是可选的。 默认情况下,启用 v8

你可以通过将 test.coverage.provider 设置为 v8istanbul 来选择覆盖工具:

vitest.config.ts
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8' // or 'istanbul'
    },
  },
})

当你启动 Vitest 进程时,它会提示你自动安装相应的支持包。

或者,如果你更喜欢手动安装它们:

bash
npm i -D @vitest/coverage-v8
bash
npm i -D @vitest/coverage-istanbul

V8 Provider

INFO

以下对 V8 覆盖率的说明仅适用于 Vitest,并不适用于其他测试工具。 从 v3.2.0 版本开始,Vitest 在 V8 覆盖率中采用了 基于 AST 的重映射技术 ,从而生成与 Istanbul 一致的覆盖率报告。

这让用户在享受 V8 覆盖率高速执行的同时,也能获得 Istanbul 覆盖率的高准确度。

Vitest 默认采用 v8 作为覆盖率提供器。 此提供器依赖于基于 V8 引擎 的 JavaScript 运行环境,比如 NodeJS、Deno,或者 Google Chrome 等 Chromium 内核的浏览器。

覆盖率收集是在程序运行时完成的,通过 node:inspector 模块以及浏览器中的 Chrome DevTools Protocol 协议 与 V8 交互即可实现。这样,用户的源码可以直接被执行,而不需要事先进行插桩处理。

  • ✅ 推荐使用该选项
  • ✅ 不需要先做转译处理,测试文件可直接运行
  • ✅ 执行速度比 Istanbul 更快
  • ✅ 占用内存比 Istanbul 更少
  • ✅ 覆盖率报告的精确度与 Istanbul 相当(自 Vitest v3.2.0 起)
  • ⚠️ 在某些场景下(如加载大量模块)可能比 Istanbul 慢,因为 V8 不支持只对特定模块收集覆盖率
  • ⚠️ 存在 V8 引擎自身的一些小限制,详见 ast-v8-to-istanbul 的限制说明
  • ❌ 不支持非 V8 环境,比如 Firefox、Bun;也不适用于不通过 profiler 提供 V8 覆盖率的环境,例如 Cloudflare Workers
测试文件启用 V8 运行时收集覆盖率运行文件从 V8 收集覆盖率结果将覆盖率结果映射到源文件生成覆盖率报告

Istanbul 覆盖率提供方案

Istanbul 代码覆盖率工具 自 2012 年发布以来,已在各种场景中得到了充分验证。 这种覆盖率提供器能在任何 JavaScript 运行环境中使用,因为它是通过在用户源码中插入额外的代码来跟踪执行情况。

简单来说,插桩就是在你的源文件里加入一段额外的 JavaScript,用于记录代码的执行路径:

js
// 分支和函数覆盖率计数器的简化示例
const coverage = { 
  branches: { 1: [0, 0] }, 
  functions: { 1: 0 }, 
} 

export function getUsername(id) {
  // 当这个函数被调用时,函数覆盖率会增加
  coverage.functions['1']++

  if (id == null) {
    // 当这个分支被调用时,分支覆盖率会增加
    coverage.branches['1'][0]++

    throw new Error('User ID is required')
  }
  // 当 if 语句条件不满足时,隐式的 else 覆盖率会增加
  coverage.branches['1'][1]++

  return database.getUser(id)
}

globalThis.__VITEST_COVERAGE__ ||= {} 
globalThis.__VITEST_COVERAGE__[filename] = coverage 
  • ✅ 可以在任何 JavaScript 环境中使用
  • ✅ 已被业界广泛采用并在 13 年中得到充分验证
  • ✅ 某些情况下执行速度优于 V8,因为插桩可以只针对特定文件,而 V8 会对所有模块插桩
  • ❌ 需要在执行前进行插桩处理
  • ❌ 由于插桩带来的额外开销,执行速度普遍比 V8 慢
  • ❌ 插桩会使文件体积变大
  • ❌ 内存消耗比 V8 更高
测试文件使用 Babel 进行预插桩运行文件从 Javascript 作用域收集覆盖率结果将覆盖率结果映射到源文件生成覆盖率报告

覆盖率配置指南

TIP

你可以在 覆盖率配置参考 中查看所有可用的覆盖率选项。

如果想要在测试中开启覆盖率统计,可以在命令行里加上 --coverage 参数,或者在 vitest.config.ts 文件里将 coverage.enabled 设置为 true

json
{
  "scripts": {
    "test": "vitest",
    "coverage": "vitest run --coverage"
  }
}
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      enabled: true
    },
  },
})

在覆盖率报告中设置需要统计或忽略的文件

你可以通过设置 coverage.includecoverage.exclude 来决定覆盖率报告中展示哪些文件。

Vitest 默认只统计测试中实际导入的文件。如果希望报告里也包含那些未被测试覆盖到的文件,需要在 coverage.include 中配置一个能匹配你源代码文件的模式:

ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      include: ['src/**/*.{ts,tsx}']
    },
  },
})
sh
├── src
   ├── components
   └── counter.tsx
   ├── mock-data
   ├── products.json
   └── users.json
   └── utils
       ├── formatters.ts
       ├── time.ts
       └── users.ts
├── test
   └── utils.test.ts

├── package.json
├── tsup.config.ts
└── vitest.config.ts

如果你想从覆盖率中排除已经被 coverage.include 匹配到的部分文件,可以通过额外配置 coverage.exclude 来实现:

ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      include: ['src/**/*.{ts,tsx}'],
      exclude: ['**/utils/users.ts']
    },
  },
})
sh
├── src
   ├── components
   └── counter.tsx
   ├── mock-data
   ├── products.json
   └── users.json
   └── utils
       ├── formatters.ts
       ├── time.ts
       └── users.ts
├── test
   └── utils.test.ts

├── package.json
├── tsup.config.ts
└── vitest.config.ts

自定义覆盖率的报告器

我们可以通过在 test.coverage.reporter 中传递软件包名称或绝对路径来使用自定义覆盖报告器:

vitest.config.ts
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      reporter: [
        // 使用 NPM 包的名称指定报告器
        ['@vitest/custom-coverage-reporter', { someOption: true }],

        // 使用本地路径指定报告器
        '/absolute/path/to/custom-reporter.cjs',
      ],
    },
  },
})

自定义报告器由 Istanbul 加载,必须与其报告器接口相匹配。查看 built-in reporters' implementation 了解更多详情。

custom-reporter.cjs
js
const { ReportBase } = require('istanbul-lib-report')

module.exports = class CustomReporter extends ReportBase {
  constructor(opts) {
    super()

    // 从配置中传递的选项在这里可用
    this.file = opts.file
  }

  onStart(root, context) {
    this.contentWriter = context.writer.writeFile(this.file)
    this.contentWriter.println('Start of custom coverage report')
  }

  onEnd() {
    this.contentWriter.println('End of custom coverage report')
    this.contentWriter.close()
  }
}

自定义覆盖率的提供者

也可以通过将 'custom' 传递给 test.coverage.provider 来配置你的自定义覆盖率提供者:

vitest.config.ts
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      provider: 'custom',
      customProviderModule: 'my-custom-coverage-provider',
    },
  },
})

自定义覆盖率提供者需要一个 customProviderModule 选项,它是一个模块名称或从中加载 CoverageProviderModule 的路径。 它必须将实现 CoverageProviderModule 的对象导出为默认导出:

my-custom-coverage-provider.ts
ts
import type {
  CoverageProvider,
  CoverageProviderModule,
  ResolvedCoverageOptions,
  Vitest,
} from 'vitest'

const CustomCoverageProviderModule: CoverageProviderModule = {
  getProvider(): CoverageProvider {
    return new CustomCoverageProvider()
  },

  // 实现 CoverageProviderModule 的其余部分...
}

class CustomCoverageProvider implements CoverageProvider {
  name = 'custom-coverage-provider'
  options!: ResolvedCoverageOptions

  initialize(ctx: Vitest) {
    this.options = ctx.config.coverage
  }

  // 实现 CoverageProvider 的其余部分...
}

export default CustomCoverageProviderModule

请参阅类型定义查看有关详细信息。

代码忽略

两个覆盖率提供商都有自己的方法来忽略覆盖率报告中的代码:

使用 TypeScript 时,源代码使用 esbuild 进行转译,这会从源代码中删除所有注释(esbuild#516)。 被视为合法注释的注释将被保留。

你可以在忽略提示里加入 @preserve 关键字。 但要小心,这些忽略提示有可能会被打包进最终的生产环境构建中。

diff
-/* istanbul ignore if */
+/* istanbul ignore if -- @preserve */
if (condition) {

-/* v8 ignore if */
+/* v8 ignore if -- @preserve */
if (condition) {

示例

ts
/* istanbul ignore start -- @preserve */
if (parameter) { 
  console.log('Ignored') 
} 
else { 
  console.log('Ignored') 
} 
/* istanbul ignore stop -- @preserve */

console.log('Included')

/* v8 ignore start -- @preserve */
if (parameter) { 
  console.log('Ignored') 
} 
else { 
  console.log('Ignored') 
} 
/* v8 ignore stop -- @preserve */

console.log('Included')
ts
/* v8 ignore if -- @preserve */
if (parameter) { 
  console.log('Ignored') 
} 
else {
  console.log('Included')
}

/* v8 ignore else -- @preserve */
if (parameter) {
  console.log('Included')
}
else { 
  console.log('Ignored') 
} 
ts
/* v8 ignore next -- @preserve */
console.log('Ignored') 
console.log('Included')

/* v8 ignore next -- @preserve */
function ignored() { 
  console.log('all') 
  console.log('lines') 
  console.log('are') 
  console.log('ignored') 
} 

/* v8 ignore next -- @preserve */
class Ignored { 
  ignored() {} 
  alsoIgnored() {} 
} 

/* v8 ignore next -- @preserve */
condition 
  ? console.log('ignored') 
  : console.log('also ignored') 
ts
/* v8 ignore next -- @preserve */
try { 
  console.log('Ignored') 
} 
catch (error) { 
  console.log('Ignored') 
} 

try {
  console.log('Included')
}
catch (error) {
  /* v8 ignore next -- @preserve */
  console.log('Ignored') 
  /* v8 ignore next -- @preserve */
  console.log('Ignored') 
}

// 由于 esbuild 不支持,需要使用 rolldown-vite。
// 参阅 https://vite.dev/guide/rolldown.html#how-to-try-rolldown
try {
  console.log('Included')
}
catch (error) /* v8 ignore next */ { 
  console.log('Ignored') 
} 
ts
switch (type) {
  case 1:
    return 'Included'

  /* v8 ignore next -- @preserve */
  case 2: 
    return 'Ignored'

  case 3:
    return 'Included'

  /* v8 ignore next -- @preserve */
  default: 
    return 'Ignored'
}
ts
/* v8 ignore file -- @preserve */
export function ignored() { 
  return 'Whole file is ignored'
}

覆盖率性能

如果你的项目中代码覆盖率生成较慢,请参阅 性能测试分析 | 代码覆盖率

UI 模式

You can check your coverage report in Vitest UI and HTML reporter.

This is integrated with builtin coverage reporters with HTML output (html, html-spa, and lcov reporters). html reporter is enabled by default and this works out of the box. To integrate with custom reporters, you can configure coverage.htmlDir.

html coverage activation in Vitest UIhtml coverage activation in Vitest UIhtml coverage in Vitest UIhtml coverage in Vitest UI