> **使用场景**:衡量 Playwright E2E 测试对应用程序代码的覆盖程度。适用于识别未经测试的代码路径,并在 CI 中强制执行覆盖率阈值。 ## 快速参考 ```bash # 安装覆盖率依赖 npm install -D nyc istanbul-lib-coverage istanbul-reports v8-to-istanbul # 运行并收集覆盖率 npx playwright test # 使用 fixture 收集覆盖率 # 从收集的数据生成报告 npx nyc report --reporter=html --reporter=text-summary npx nyc report --reporter=lcov # 用于 CI 集成(Codecov、SonarQube) # 检查覆盖率阈值 npx nyc check-coverage --lines 80 --branches 70 --functions 75 ``` ## 模式 ### 模式 1:V8 覆盖率 + Playwright(推荐) **适用场景**:在 E2E 测试期间测量 Web 应用程序的代码覆盖率。V8 覆盖率利用 Chrome DevTools 协议实现准确的 JavaScript 覆盖率。 **避免场景**:测试不受你控制的第三方站点。 **第 1 步:创建覆盖率 fixture:** ```ts // fixtures/coverage.ts import { test as base, expect } from "@playwright/test" import * as fs from "fs" import * as path from "path" import * as crypto from "crypto" const coverageDir = path.resolve(process.cwd(), ".nyc_output") export const test = base.extend({ page: async ({ page, browserName }, use, testInfo) => { // V8 覆盖率仅在 Chromium 中有效 if (browserName === "chromium") { await page.coverage.startJSCoverage({ resetOnNavigation: false }) } await use(page) if (browserName === "chromium") { const coverage = await page.coverage.stopJSCoverage() // 过滤出仅属于应用程序的代码 const appCoverage = coverage.filter( (entry) => entry.url.includes("localhost") && !entry.url.includes("node_modules") ) if (appCoverage.length > 0) { // 确保输出目录存在 fs.mkdirSync(coverageDir, { recursive: true }) // 写入 V8 覆盖率数据 const coverageFile = path.join(coverageDir, `coverage-${crypto.randomUUID()}.json`) fs.writeFileSync( coverageFile, JSON.stringify({ result: appCoverage.map((entry) => ({ scriptId: "0", url: entry.url, functions: entry.functions || [], })), }) ) } } }, }) export { expect } ``` **第 2 步:在测试中使用覆盖率 fixture:** ```ts // tests/dashboard.spec.ts import { test, expect } from "../fixtures/coverage" test("仪表盘加载小部件", async ({ page }) => { await page.goto("/dashboard") await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible() // 覆盖率会自动收集并保存 }) ``` **第 3 步:测试后生成覆盖率报告:** ```json // package.json { "scripts": { "test:e2e": "npx playwright test", "test:coverage": "npx playwright test --project=chromium && npx nyc report --reporter=html --reporter=text-summary", "coverage:check": "npx nyc check-coverage --lines 80 --branches 70 --functions 75" } } ``` ### 模式 2:Istanbul(基于 Source Map)覆盖率 **适用场景**:你的应用程序使用 webpack、Vite 或其他打包工具,并且你需要与原始源文件关联的 source map 覆盖率。 **避免场景**:V8 覆盖率已经足够——它的设置更简单。 **第 1 步:对应用程序代码进行插桩:** 对于 **Vite**: ```bash npm install -D vite-plugin-istanbul ``` ```ts // vite.config.ts import { defineConfig } from "vite" import istanbul from "vite-plugin-istanbul" export default defineConfig({ plugins: [ ...(process.env.COVERAGE === "true" ? [ istanbul({ include: "src/*", exclude: ["node_modules", "test/"], extension: [".js", ".ts", ".tsx", ".jsx"], requireEnv: true, }), ] : []), ], }) ``` 对于 **webpack**(Create React App): ```bash npm install -D @istanbuljs/nyc-config-typescript babel-plugin-istanbul ``` ```json // babel.config.json(或 .babelrc) { "env": { "test": { "plugins": ["istanbul"] } } } ``` **第 2 步:从 `window.__coverage__` 收集覆盖率:** ```ts // fixtures/istanbul-coverage.ts import { test as base, expect } from "@playwright/test" import * as fs from "fs" import * as path from "path" import * as crypto from "crypto" const coverageDir = path.resolve(process.cwd(), ".nyc_output") export const test = base.extend({ page: async ({ page }, use) => { await use(page) // Istanbul 对代码进行插桩,并将覆盖率暴露在 window.__coverage__ 上 const coverage = await page.evaluate(() => (window as any).__coverage__) if (coverage) { fs.mkdirSync(coverageDir, { recursive: true }) const coverageFile = path.join(coverageDir, `coverage-${crypto.randomUUID()}.json`) fs.writeFileSync(coverageFile, JSON.stringify(coverage)) } }, }) export { expect } ``` **第 3 步:配置 nyc:** ```json // .nycrc.json { "extends": "@istanbuljs/nyc-config-typescript", "all": true, "include": ["src/**/*.{ts,tsx,js,jsx}"], "exclude": ["src/**/*.test.*", "src/**/*.spec.*", "src/test/**", "src/**/*.d.ts"], "reporter": ["html", "text-summary", "lcov"], "report-dir": "coverage" } ``` ```bash # 使用插桩启动应用 COVERAGE=true npm run dev # 运行测试(在另一个终端中,或等待开发服务器启动后) npx playwright test --project=chromium # 生成报告 npx nyc report ``` ### 模式 3:CI 中的覆盖率阈值 **适用场景**:将最低覆盖率作为质量门禁强制执行。 **避免场景**:你才刚刚开始接触覆盖率——请在建立基线之后再设置阈值。 ```json // .nycrc.json { "check-coverage": true, "lines": 80, "branches": 70, "functions": 75, "statements": 80, "reporter": ["html", "text-summary", "lcov"] } ``` **带覆盖率检查的 GitHub Actions:** ```yaml # .github/workflows/playwright.yml(添加到测试任务步骤中) - name: 运行带覆盖率的 Playwright 测试 run: COVERAGE=true npx playwright test --project=chromium - name: 生成覆盖率报告 run: npx nyc report --reporter=html --reporter=text-summary --reporter=lcov if: ${{ !cancelled() }} - name: 检查覆盖率阈值 run: npx nyc check-coverage --lines 80 --branches 70 --functions 75 - name: 上传覆盖率报告 uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: coverage-report path: coverage/ retention-days: 14 ``` ### 模式 4:合并来自分片运行的覆盖率 **适用场景**:测试在多台 CI 机器上分片运行,你需要聚合后的覆盖率。 **避免场景**:测试在单台机器上运行。 ```yaml # .github/workflows/playwright.yml jobs: test: strategy: fail-fast: false matrix: shard: [1/4, 2/4, 3/4, 4/4] steps: # ... 检出、安装等 - name: 运行带覆盖率的分片测试 run: COVERAGE=true npx playwright test --shard=${{ matrix.shard }} --project=chromium - name: 上传覆盖率数据 uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: coverage-${{ strategy.job-index }} path: .nyc_output/ retention-days: 1 merge-coverage: needs: test if: ${{ !cancelled() }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - run: npm ci - name: 下载所有覆盖率数据 uses: actions/download-artifact@v4 with: path: all-coverage pattern: coverage-* merge-multiple: true - name: 合并覆盖率 run: | mkdir -p .nyc_output cp all-coverage/*.json .nyc_output/ npx nyc report --reporter=html --reporter=text-summary --reporter=lcov npx nyc check-coverage --lines 80 --branches 70 --functions 75 - name: 上传合并后的覆盖率报告 uses: actions/upload-artifact@v4 with: name: coverage-report path: coverage/ retention-days: 14 ``` ### 模式 5:CSS 覆盖率 **适用场景**:识别未使用的 CSS 以减小打包体积。 **避免场景**:CSS 对你的应用程序来说不是性能问题。 ```ts // fixtures/css-coverage.ts import { test as base, expect } from "@playwright/test" import * as fs from "fs" import * as path from "path" export const test = base.extend({ page: async ({ page, browserName }, use, testInfo) => { if (browserName === "chromium") { await page.coverage.startCSSCoverage() } await use(page) if (browserName === "chromium") { const coverage = await page.coverage.stopCSSCoverage() // 计算每个文件中未使用的 CSS 百分比 const report = coverage.map((entry) => { const totalBytes = entry.text.length const usedBytes = entry.ranges.reduce((acc, range) => acc + (range.end - range.start), 0) return { url: entry.url, totalBytes, usedBytes, unusedPercent: ((1 - usedBytes / totalBytes) * 100).toFixed(1), } }) // 保存以供分析 const outputDir = path.resolve(process.cwd(), "css-coverage") fs.mkdirSync(outputDir, { recursive: true }) fs.writeFileSync( path.join(outputDir, `${testInfo.testId}.json`), JSON.stringify(report, null, 2) ) } }, }) export { expect } ``` ## 决策指南 | 覆盖率类型 | 工具 | 衡量内容 | 设置难度 | 准确性 | | -------------------------- | ------------------------------------------------ | --------------------- | -------- | -------------------- | | V8(Chrome DevTools) | `page.coverage` API | 浏览器中的 JS 执行 | 低 | 高(运行时) | | Istanbul(插桩) | `babel-plugin-istanbul` / `vite-plugin-istanbul` | 带 source map 的 JS | 中 | 高(源码级别) | | CSS | `page.coverage.startCSSCoverage()` | CSS 规则使用情况 | 低 | 高 | | 指标 | 衡量内容 | 推荐阈值 | 原因 | | ---------- | ---------------------- | -------- | -------------------------------------------------- | | Lines | 已执行的代码行数 | 80%+ | 最直观;可发现死代码 | | Branches | 已覆盖的 if/else 路径 | 70%+ | 可发现未测试的条件分支 | | Functions | 已调用的函数 | 75%+ | 可发现未使用的函数 | | Statements | 已执行的单个语句 | 80%+ | 与 lines 类似,但统计多语句行的每条语句 | | 场景 | 方案 | 原因 | | ------------------------------ | -------------------------------- | -------------------------------------- | | 刚刚开始接触覆盖率 | V8 覆盖率,不设阈值 | 先建立基线 | | 成熟的代码库 | Istanbul + CI 中的阈值 | 基于 source map,强制执行 | | 分片 CI | 合并所有分片后的 `.nyc_output` | 聚合后的覆盖率视图 | | 只关心 Chromium | V8 覆盖率 fixture | 设置最简单,无需修改构建 | | 需要源码级别的准确性 | Istanbul + 打包工具插件 | 映射回原始源文件 | | 上传到 Codecov / SonarQube | 生成 `lcov` 格式 | 覆盖率服务的标准格式 | ## 反模式 | 反模式 | 问题 | 推荐做法 | | ------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------ | | 追求 100% E2E 覆盖率 | E2E 测试成本高,超过 80% 后收益递减 | 设定合理的阈值;用单元测试覆盖边界情况 | | 在所有浏览器中收集覆盖率 | V8 覆盖率仅在 Chromium 中有效,其他浏览器浪费资源 | 仅在 `--project=chromium` 下运行覆盖率收集 | | 未过滤源文件 | 覆盖率包含 `node_modules` 和第三方代码 | 过滤:`entry.url.includes('localhost') && !entry.url.includes('node_modules')` | | 覆盖率拖慢 CI 却未产生价值 | 在每个 PR 上都收集覆盖率但没有设置阈值 | 要么强制执行阈值,要么跳过覆盖率收集 | | 在生产构建中启用 Istanbul 插桩 | 性能开销;暴露内部实现 | 仅在 `COVERAGE=true` 时进行插桩 | | 仅靠 E2E 覆盖率衡量 | E2E 测试覆盖的是 happy path,单元测试覆盖的是分支 | 将 E2E 覆盖率与单元测试覆盖率结合使用 | ## 故障排除 ### 覆盖率报告显示 0% 或为空 **原因(V8)**:测试未使用覆盖率 fixture,或者未调用 `startJSCoverage()`。 **修复**:确保测试从覆盖率 fixture 文件导入,而不是直接从 `@playwright/test` 导入: ```ts // 使用这个: import { test, expect } from "../fixtures/coverage" // 不要用这个: import { test, expect } from "@playwright/test" ``` **原因(Istanbul)**:应用程序未进行插桩,或者 `window.__coverage__` 为 undefined。 **修复**:确认应用程序已启用插桩运行: ```bash # 检查覆盖率是否已暴露 COVERAGE=true npm run dev # 在浏览器控制台中:window.__coverage__ -> 应该是一个对象,而不是 undefined ``` ### 覆盖率文件无法正确合并 **原因**:来自不同分片的覆盖率 JSON 文件格式不兼容或键重叠。 **修复**:确保所有分片使用唯一的文件名写入 `.nyc_output/`: ```ts const coverageFile = path.join(coverageDir, `coverage-${crypto.randomUUID()}.json`) ``` 然后使用 `npx nyc report` 合并(它会读取 `.nyc_output/` 中的所有文件)。 ### V8 覆盖率无法映射回源文件 **原因**:V8 覆盖率报告的是打包/编译后的 URL,而不是原始源文件。 **修复**:改用 Istanbul 插桩以获得源码级别的准确性,或者使用 `v8-to-istanbul` 进行转换: ```bash npm install -D v8-to-istanbul ``` ### 并行运行时代码覆盖率下降 **原因**:如果文件名冲突,并行 worker 会互相覆盖覆盖率文件。 **修复**:在文件名中包含 `workerInfo.workerIndex` 或 UUID: ```ts const coverageFile = path.join( coverageDir, `coverage-worker${workerInfo.workerIndex}-${crypto.randomUUID()}.json` ) ``` ## 相关文档 - [ci/parallel-and-sharding.md](parallel-and-sharding.md) —— 从分片运行中合并覆盖率 - [ci/ci-github-actions.md](ci-github-actions.md) —— CI 中的覆盖率阈值 - [ci/reporting-and-artifacts.md](reporting-and-artifacts.md) —— 上传覆盖率报告 - [core/configuration.md](../core/configuration.md) —— 覆盖率的项目配置 - [core/test-architecture.md](../core/test-architecture.md) —— E2E 与单元测试覆盖率策略