--- url: /guide/coverage.md --- # 覆盖率 Vitest 支持通过 [`v8`](https://v8.dev/blog/javascript-code-coverage) 的原生代码覆盖率,以及通过 [`istanbul`](https://istanbul.js.org/) 的插桩代码覆盖率。 ## 覆盖率提供者 `v8` 和 `istanbul` 支持都是可选的。默认情况下,将使用 `v8`。 你可以通过将 `test.coverage.provider` 设置为 `v8` 或 `istanbul` 来选择覆盖率工具: ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { provider: 'v8' // 或 'istanbul' }, }, }) ``` 当你启动 Vitest 进程时,它会提示你自动安装相应的支持包。 或者如果你更喜欢手动安装它们: ::: code-group ```bash [v8] npm i -D @vitest/coverage-v8 ``` ```bash [istanbul] npm i -D @vitest/coverage-istanbul ``` ::: ## V8 提供者 ::: info 下面关于 V8 覆盖率的描述是 Vitest 特有的,不适用于其他测试运行器。 自 `v3.2.0` 以来,Vitest 对 V8 覆盖率使用了 [基于 AST 的覆盖率重映射](/blog/vitest-3-2#coverage-v8-ast-aware-remapping),生成的覆盖率报告与 Istanbul 相同。 这允许用户拥有 V8 覆盖率的速度和 Istanbul 覆盖率的准确性。 ::: 默认情况下,Vitest 使用 `'v8'` 覆盖率提供者。 此提供者需要基于 [V8 引擎](https://v8.dev/) 实现的 Javascript 运行时,例如 NodeJS、Deno 或任何基于 Chromium 的浏览器(如 Google Chrome)。 覆盖率收集是在运行时通过 [`node:inspector`](https://nodejs.org/api/inspector.html) 指示 V8 以及在浏览器中使用 [Chrome DevTools 协议](https://chromedevtools.github.io/devtools-protocol/tot/Profiler/) 进行的。用户的源文件可以直接执行,无需任何预插桩步骤。 * ✅ 推荐使用的选项 * ✅ 无需预转译步骤。测试文件可以直接执行。 * ✅ 执行速度比 Istanbul 快。 * ✅ 内存使用量比 Istanbul 低。 * ✅ 覆盖率报告准确性与 Istanbul 一样好([自 Vitest `v3.2.0` 起](/blog/vitest-3-2#coverage-v8-ast-aware-remapping))。 * ⚠️ 在某些情况下可能比 Istanbul 慢,例如加载许多不同模块时。V8 不支持将覆盖率收集限制在特定模块。 * ⚠️ V8 引擎设置了一些轻微的限制。参见 [`ast-v8-to-istanbul` | 限制](https://github.com/AriPerkkio/ast-v8-to-istanbul?tab=readme-ov-file#limitations)。 * ❌ 不适用于不使用 V8 的环境,例如 Firefox 或 Bun。或者不适用于不通过 profiler 暴露 V8 覆盖率的环境,例如 Cloudflare Workers。 ## Istanbul 提供者 [Istanbul 代码覆盖率工具](https://istanbul.js.org/) 自 2012 年以来一直存在,且经过了非常充分的实战检验。这个提供者可在任何 JavaScript 运行时上工作,因为覆盖率跟踪是通过转换你的源代码来添加插桩逻辑实现的。实际上,Vitest 最终运行的代码大致会像这样: ```js // 分支和函数覆盖率计数器的简化示例 const coverage = { // [!code ++] branches: { 1: [0, 0] }, // [!code ++] functions: { 1: 0 }, // [!code ++] } // [!code ++] export function getUsername(id) { // 当此被调用时函数覆盖率增加 // [!code ++] coverage.functions['1']++ // [!code ++] if (id == null) { // 当此被调用时分支覆盖率增加 // [!code ++] coverage.branches['1'][0]++ // [!code ++] throw new Error('User ID is required') } // 当 if 语句条件不满足时隐式 else 覆盖率增加 // [!code ++] coverage.branches['1'][1]++ // [!code ++] return database.getUser(id) } globalThis.__VITEST_COVERAGE__ ||= {} // [!code ++] globalThis.__VITEST_COVERAGE__[filename] = coverage // [!code ++] ``` * ✅ 适用于任何 Javascript 运行时 * ✅ 广泛使用,并且经过了 13 年以上的实战检验。 * ✅ 在某些情况下比 V8 更快。覆盖率插桩可以限制在特定文件上,而 V8 会对所有模块进行插桩。 * ❌ 在运行之前,源代码会被转换以添加插桩 * ❌ 由于插桩开销,执行速度比 V8 更慢 * ❌ 内存使用比 V8 更高 ## 覆盖率设置 ::: tip 所有覆盖率选项都列在 [覆盖率配置参考](/config/coverage) 中。 ::: 要启用覆盖率进行测试,你可以在 CLI 中传递 `--coverage` 标志或在 `vitest.config.ts` 中设置 `coverage.enabled`: ::: code-group ```json [package.json] { "scripts": { "test": "vitest", "coverage": "vitest run --coverage" } } ``` ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { enabled: true }, }, }) ``` ::: ## 在覆盖率报告中包含和排除文件 你可以通过配置 [`coverage.include`](/config/coverage#coverage-include) 和 [`coverage.exclude`](/config/coverage#coverage-exclude) 来定义哪些文件显示在覆盖率报告中。 默认情况下,Vitest 只会显示在测试运行期间导入的文件。 要将未覆盖的文件包含在报告中,你需要使用能匹配源文件的模式配置 [`coverage.include`](/config/coverage#coverage-include): ::: code-group ```ts [vitest.config.ts] {6} import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { include: ['src/**/*.{ts,tsx}'] }, }, }) ``` ```sh [被覆盖的文件] ├── src │ ├── components │ │ └── counter.tsx # [!code ++] │ ├── mock-data │ │ ├── products.json # [!code error] │ │ └── users.json # [!code error] │ └── utils │ ├── formatters.ts # [!code ++] │ ├── time.ts # [!code ++] │ └── users.ts # [!code ++] ├── test │ └── utils.test.ts # [!code error] │ ├── package.json # [!code error] ├── tsup.config.ts # [!code error] └── vitest.config.ts # [!code error] ``` ::: 要排除匹配 `coverage.include` 的文件,你可以定义额外的 [`coverage.exclude`](/config/coverage#coverage-exclude): ::: code-group ```ts [vitest.config.ts] {7} import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { include: ['src/**/*.{ts,tsx}'], exclude: ['**/utils/users.ts'] }, }, }) ``` ```sh [被覆盖的文件] ├── src │ ├── components │ │ └── counter.tsx # [!code ++] │ ├── mock-data │ │ ├── products.json # [!code error] │ │ └── users.json # [!code error] │ └── utils │ ├── formatters.ts # [!code ++] │ ├── time.ts # [!code ++] │ └── users.ts # [!code error] ├── test │ └── utils.test.ts # [!code error] │ ├── package.json # [!code error] ├── tsup.config.ts # [!code error] └── vitest.config.ts # [!code error] ``` ::: ## 自定义覆盖率报告器 你可以通过在 `test.coverage.reporter` 中传递包名或绝对路径来使用自定义覆盖率报告器: ```ts [vitest.config.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', ], }, }, }) ``` 自定义报告器由 `@vitest/istanbul-lib-report` 加载,且必须符合其报告器接口。实现参考请参阅[内置报告器](https://github.com/vitest-dev/istanbuljs/tree/main/packages/istanbul-lib-report/src/reports)。 ::: code-group ```js [custom-reporter.mjs] import { ReportBase } from '@vitest/istanbul-lib-report' export default class CustomReporter extends ReportBase { constructor(opts) { super() if (!opts.file) { throw new Error('File is required as custom reporter parameter') } this.file = opts.file } onStart(root, context) { this.contentWriter = context.writer.writeFile(this.file) this.contentWriter.println('Start of custom coverage report ESM') } onEnd() { this.contentWriter.println('End of custom coverage report ESM') this.contentWriter.close() } } ``` ```js [custom-reporter.cjs] const { ReportBase } = require('@vitest/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('自定义覆盖率报告开始') } onEnd() { this.contentWriter.println('自定义覆盖率报告结束') this.contentWriter.close() } } ``` ::: ## 自定义覆盖率提供者 也可以通过在 `test.coverage.provider` 中传递 `'custom'` 来提供自定义覆盖率提供者: ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { provider: 'custom', customProviderModule: 'my-custom-coverage-provider' }, }, }) ``` 自定义提供者需要一个 `customProviderModule` 选项,这是一个模块名或路径,用于加载 `CoverageProviderModule`。它必须导出一个实现 `CoverageProviderModule` 的对象作为默认导出: ```ts [my-custom-coverage-provider.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 ``` 请参阅类型定义以获取更多详细信息。 ## 忽略代码 两种覆盖率提供者都有各自的方法来忽略覆盖率报告中的代码: * [`v8`](https://github.com/AriPerkkio/ast-v8-to-istanbul?tab=readme-ov-file#ignoring-code) * [`istanbul`](https://github.com/istanbuljs/nyc#parsing-hints-ignoring-lines) 当使用 TypeScript 时,源代码会使用 `esbuild` 进行转译,它会剥离源代码中的所有注释 ([esbuild#516](https://github.com/evanw/esbuild/issues/516))。 被视为 [合法注释](https://esbuild.github.io/api/#legal-comments) 的注释会被保留。 你可以在忽略提示中包含 `@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) { ``` ### 示例 ::: code-group ```ts [lines: start/stop] /* istanbul ignore start -- @preserve */ if (parameter) { // [!code error] console.log('Ignored') // [!code error] } // [!code error] else { // [!code error] console.log('Ignored') // [!code error] } // [!code error] /* istanbul ignore stop -- @preserve */ console.log('Included') /* v8 ignore start -- @preserve */ if (parameter) { // [!code error] console.log('Ignored') // [!code error] } // [!code error] else { // [!code error] console.log('Ignored') // [!code error] } // [!code error] /* v8 ignore stop -- @preserve */ console.log('Included') ``` ```ts [if else] /* v8 ignore if -- @preserve */ if (parameter) { // [!code error] console.log('Ignored') // [!code error] } // [!code error] else { console.log('Included') } /* v8 ignore else -- @preserve */ if (parameter) { console.log('Included') } else { // [!code error] console.log('Ignored') // [!code error] } // [!code error] ``` ```ts [next node] /* v8 ignore next -- @preserve */ console.log('Ignored') // [!code error] console.log('Included') /* v8 ignore next -- @preserve */ function ignored() { // [!code error] console.log('all') // [!code error] // [!code error] console.log('lines') // [!code error] // [!code error] console.log('are') // [!code error] // [!code error] console.log('ignored') // [!code error] } // [!code error] /* v8 ignore next -- @preserve */ class Ignored { // [!code error] ignored() {} // [!code error] alsoIgnored() {} // [!code error] } // [!code error] /* v8 ignore next -- @preserve */ condition // [!code error] ? console.log('ignored') // [!code error] : console.log('also ignored') // [!code error] ``` ```ts [try catch] /* v8 ignore next -- @preserve */ try { // [!code error] console.log('Ignored') // [!code error] } // [!code error] catch (error) { // [!code error] console.log('Ignored') // [!code error] } // [!code error] try { console.log('Included') } catch (error) { /* v8 ignore next -- @preserve */ console.log('Ignored') // [!code error] /* v8 ignore next -- @preserve */ console.log('Ignored') // [!code error] } // 由于 esbuild 缺乏支持,需要 rolldown-vite。 // 参见 https://vite.dev/guide/rolldown.html#how-to-try-rolldown try { console.log('Included') } catch (error) /* v8 ignore next */ { // [!code error] console.log('Ignored') // [!code error] } // [!code error] ``` ```ts [switch case] switch (type) { case 1: return 'Included' /* v8 ignore next -- @preserve */ case 2: // [!code error] return 'Ignored' // [!code error] case 3: return 'Included' /* v8 ignore next -- @preserve */ default: // [!code error] return 'Ignored' // [!code error] } ``` ```ts [whole file] /* v8 ignore file -- @preserve */ export function ignored() { // [!code error] return 'Whole file is ignored'// [!code error] }// [!code error] ``` ::: ## 覆盖率性能 如果项目生成代码覆盖率较慢,请参阅[分析测试性能 | 代码覆盖率](/guide/profiling-test-performance#code-coverage)。 ## Vitest UI 你可以在 [Vitest UI](/guide/ui) 和 [HTML 报告器](/guide/reporters#html-reporter)中查看覆盖率报告。 这与具有 HTML 输出的内置覆盖率报告器集成(`html`、`html-spa` 和 `lcov` 报告器)。`html` 报告器默认启用,开箱即用。要与自定义报告器集成,你可以配置 [`coverage.htmlDir`](/config/coverage#coverage-htmldir)。 ## 代理环境中的覆盖率 当 Vitest 检测到它正在 AI 编码代理中运行时,它会自动调整默认的 `text` 报告器,以减少输出并最小化 token 使用量: * 在 `text` 报告器上设置 `skipFull: true`,因此覆盖率达到 100% 的文件会从终端输出中省略。 * [`text-summary`](/config/coverage#coverage-reporter) 报告器会自动添加,因此即使 `skipFull` 隐藏了所有单个文件,代理也始终能看到一个简洁的总计表。 这些调整仅在 `text` 报告器已经是活动报告器列表的一部分时才会生效(默认情况下已包含)。显式配置的报告器绝不会被移除。