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
Test fileEnable V8 runtime coverage collectionRun fileCollect coverage results from V8Remap coverage results to source filesCoverage report

Istanbul 覆盖率提供方案。

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

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

js
// Simplified example of branch and function coverage counters
const coverage = { 
  branches: { 1: [0, 0] }, 
  functions: { 1: 0 }, 
} 

export function getUsername(id) {
  // Function coverage increased when this is invoked
  coverage.functions['1']++

  if (id == null) {
    // Branch coverage increased when this is invoked
    coverage.branches['1'][0]++

    throw new Error('User ID is required')
  }
  // Implicit else coverage increased when if-statement condition not met
  coverage.branches['1'][1]++

  return database.getUser(id)
}

globalThis.__VITEST_COVERAGE__ ||= {} 
globalThis.__VITEST_COVERAGE__[filename] = coverage 
  • ✅ 可以在任何 JavaScript 环境中使用
  • ✅ 已被业界广泛采用并在 13 年中得到充分验证
  • ✅ 某些情况下执行速度优于 V8,因为插桩可以只针对特定文件,而 V8 会对所有模块插桩
  • ❌ 需要在执行前进行插桩处理
  • ❌ 由于插桩带来的额外开销,执行速度普遍比 V8 慢
  • ❌ 插桩会使文件体积变大
  • ❌ 内存消耗比 V8 更高
Test filePre‑instrumentation with BabelRun fileCollect coverage results from Javascript scopeRemap coverage results to source filesCoverage report

覆盖率配置指南。

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: [
        // Specify reporter using name of the NPM package
        ['@vitest/custom-coverage-reporter', { someOption: true }],

        // Specify reporter using local path
        '/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()

    // Options passed from configuration are available here
    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()
  },

  // Implements rest of the CoverageProviderModule ...
}

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

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

  // Implements rest of the 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) {

Coverage Performance

If code coverage generation is slow on your project, see Profiling Test Performance | Code coverage.

Vitest UI

我们可以在 Vitest UI 中查看你的覆盖率报告。

Vitest UI 会在以下情况下启用覆盖率报告:

  • 显式启用覆盖率报告:在配置文件中设置 coverage.enabled=true ,或运行 Vitest 时添加 --coverage.enabled=true 标志。
  • 添加 HTML 报告器:将 html 添加到 coverage.reporter 列表中,我们还可以启用 subdir 选项,将覆盖率报告放在子目录中。
html coverage activation in Vitest UIhtml coverage activation in Vitest UIhtml coverage in Vitest UIhtml coverage in Vitest UI

Released under the MIT License.