第20章 Vitest 第20章:代码覆盖率详解
Coverage Vitest 支持通过 V8 进行原生代码覆盖率分析,以及通过 Istanbul 进行插桩代码覆盖率分析。 覆盖率提供商 V8 和 Istanbul 的支持均为可选项。默认情况下,Vitest 会使用 V8。你可以通过设置 test.coverage.provider 为 v8 或 istanbul 来指定覆盖率工具: vitest.config.tsts import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { provider: 'v8' // 或 'istanbul' }, }, }) 启动 Vitest 进程时,若未安装对应依赖,系统会自动提示你安装支持包。也可以手动安装: v8 npm i -D @vitest/coverage-v8 istanbul npm i -D @vitest/coverage-istanbul V8 提供商 INFO 下方关于 V8 覆盖率的说明仅适用于 Vitest,并不适用于其他测试运行器。自 v3.2.0 起,Vitest 在 V8 覆盖率分析中采用了基于 AST 的覆盖率映射技术,从而生成与 Istanbul 完全一致的覆盖率报告。 这使得用户能够兼具 V8 的高速性能与 Istanbul 的精确度。Vitest 默认使用 'v8' 覆盖率提供商。该提供商要求 JavaScript 运行时底层基于 V8 引擎实现,例如 NodeJS、Deno 或任何基于 Chromium 的浏览器(如 Google Chrome)。 覆盖率收集在运行时进行:在浏览器中通过 Chrome DevTools Protocol,在 Node 环境中通过 node:inspector 指导 V8 引擎工作。用户的源文件可以直接执行,无需任何预插桩步骤。 ✅ 推荐使用的选项 ✅ 无需预转译步骤,测试文件可直接执行。 ✅ 执行速度比 Istanbul 更快。 ✅ 内存占用比 Istanbul 更低。 ✅ 自 Vitest v3.2.0 起,覆盖率报告的精确度已媲美 Istanbul。 ⚠️ 在某些情况下可能比 Istanbul 慢,例如加载大量不同模块时。V8 不支持将覆盖率收集限制在特定模块。 ⚠️ V8 引擎存在某些细微限制。详见 ast-v8-to-istanbul | Limitations。 ❌ 在不使用 V8 的环境中无法工作,例如 Firefox 或 Bun。或在未通过 profiler 暴露 V8 覆盖率的环境中也无法工作,例如 Cloudflare Workers。 工作流程:测试文件 -> 启用 V8 运行时覆盖率收集 -> 运行文件 -> 从 V8 收集覆盖率结果 -> 将覆盖率结果映射回源文件 -> 生成覆盖率报告 Istanbul 提供商 Istanbul 代码覆盖率工具自 2012 年起存在,经过了充分的实战检验。该提供商适用于任何 JavaScript 运行时,因为其覆盖率跟踪是通过转换源代码并添加插桩逻辑来实现的。实际上,Vitest 最终执行的代码结构大致如下: 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 对所有模块进行插桩不同,Istanbul 可以将插桩限制在特定文件。 ❌ 源代码在运行前会被转换以添加插桩 ❌ 由于插桩开销,执行速度比 V8 慢 ❌ 内存占用比 V8 高 工作流程:测试文件 -> 使用 Babel 进行预插桩 -> 运行文件 -> 从 JavaScript 作用域收集覆盖率结果 -> 将覆盖率结果映射回源文件 -> 生成覆盖率报告 覆盖率配置 TIP 所有覆盖率选项均列于 覆盖率配置参考 中。 要在启用覆盖率的情况下运行测试,可以在 CLI 中传入 --coverage 标志,或在 vitest.config.ts 中设置 coverage.enabled: package.json { "scripts": { "test": "vitest", "coverage": "vitest run --coverage" } } vitest.config.ts import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { enabled: true }, }, }) 包含与排除文件 你可以通过配置 coverage.include 和 coverage.exclude 来定义显示在覆盖率报告中的文件。 默认情况下,Vitest 仅显示测试运行期间被导入的文件。若要将在报告中包含未被覆盖的文件,你需要使用能够匹配源文件的模式来配置 coverage.include: vitest.config.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: vitest.config.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.tsts 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 加载,必须实现其报告器接口。可参考内置报告器的实现。 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() } } 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('Start of custom coverage report') } onEnd() { this.contentWriter.println('End of custom coverage report') this.contentWriter.close() } } 自定义覆盖率提供商 你还可以通过在 test.coverage.provider 中传入 'custom' 来提供自定义覆盖率提供商: vitest.config.tsts import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { provider: 'custom', customProviderModule: 'my-custom-coverage-provider' }, }, }) 自定义提供商需要 customProviderModule 选项,它是一个模块名称或路径,用于加载 CoverageProviderModule。该模块必须默认导出一个实现了 CoverageProviderModule 的对象: my-custom-coverage-provider.tsts 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 istanbul 在使用 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) { 示例 行:开始/结束 | if else | 下一节点 | try catch | switch case | 整个文件 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') } // 需要 rolldown-vite,因为 esbuild 不支持此功能。 // 见 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 UI 和 HTML 报告器中查看覆盖率报告。 这与内置的具有 HTML 输出的覆盖率报告器(html、html-spa 和 lcov)集成。html 报告器默认启用且开箱即用。若要与自定义报告器集成,你可以配置 coverage.htmlDir。 在 Agent 环境中的覆盖率 当 Vitest 检测到其正在 AI 编码 Agent 内部运行时,会自动调整默认文本报告器以减少输出并最小化 token 使用: 对文本报告器设置 skipFull: true,因此 100% 覆盖率的文件不会显示在终端输出中。自动添加 text-summary 报告器,以便即使 skipFull 隐藏了所有单个文件,Agent 也能始终看到简明的汇总表格。这些调整仅适用于文本报告器已经在激活报告器列表中的情况(它包含在默认配置中)。显式配置的 报告器永远不会被移除。