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' // 或 'istanbul'
    },
  },
})

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

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

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

V8 提供者

INFO

下面关于 V8 覆盖率的描述是 Vitest 特有的,不适用于其他测试运行器。 自 v3.2.0 以来,Vitest 对 V8 覆盖率使用了 基于 AST 的覆盖率重映射,生成的覆盖率报告与 Istanbul 相同。

这允许用户拥有 V8 覆盖率的速度和 Istanbul 覆盖率的准确性。

默认情况下,Vitest 使用 'v8' 覆盖率提供者。 此提供者需要基于 V8 引擎 实现的 Javascript 运行时,例如 NodeJS、Deno 或任何基于 Chromium 的浏览器(如 Google Chrome)。

覆盖率收集是在运行时通过 node:inspector 指示 V8 以及在浏览器中使用 Chrome DevTools 协议 进行的。用户的源文件可以直接执行,无需任何预插桩步骤。

  • ✅ 推荐使用的选项
  • ✅ 无需预转译步骤。测试文件可以直接执行。
  • ✅ 执行速度比 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

所有覆盖率选项都列在 覆盖率配置参考 中。

要启用覆盖率进行测试,你可以在 CLI 中传递 --coverage 标志或在 vitest.config.ts 中设置 coverage.enabled

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 加载,必须匹配其报告器接口。参见 内置报告器的实现 作为参考。

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()
  }
}

自定义覆盖率提供者

也可以通过在 test.coverage.provider 中传递 'custom' 来提供自定义覆盖率提供者:

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 关键字。 请注意,这些忽略提示现在也可能包含在最终的生产构建中。

TIP

关注 https://github.com/vitest-dev/vitest/issues/2021 以获取有关 @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'
}

覆盖率性能

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

Vitest UI

你可以在 Vitest UIHTML 报告器 中查看覆盖率报告。

这与具有 HTML 输出的内置覆盖率报告器集成(htmlhtml-spalcov 报告器)。html 报告器默认启用,开箱即用。要与自定义报告器集成,你可以配置 coverage.htmlDir

Vitest UI 中的 html 覆盖率激活Vitest UI 中的 html 覆盖率激活html coverage in Vitest UIhtml coverage in Vitest UI

Coverage in Agent Environments

When Vitest detects it is running inside an AI coding agent, it automatically adjusts the default text reporter to reduce output and minimize token usage:

  • skipFull: true is set on the text reporter, so files with 100% coverage are omitted from the terminal output.
  • The text-summary reporter is added automatically, so the agent always sees a concise totals table even when skipFull hides all individual files.

These adjustments only apply when the text reporter is already part of the active reporter list (it is included in the default). Explicitly configured reporters are never removed.