Playwright自动化测试覆盖率实战:从Istanbul插桩到CI集成
1. 项目概述为什么我们需要关注测试覆盖率如果你正在用 Playwright 写自动化测试或者正准备开始那你肯定遇到过这样的场景辛辛苦苦写了几百个测试用例跑起来一片绿色感觉稳了。但上线后一个不起眼的角落出了个线上 Bug一查那个功能路径压根就没被你的测试覆盖到。这种“测试盲区”带来的不安全感正是驱动我们引入“测试覆盖率”概念的核心原因。这个“Playwright 测试覆盖率演示项目指南”就是来解决这个痛点的。它不是一个简单的工具使用说明书而是一个从零到一教你如何将测试覆盖率分析深度集成到 Playwright 自动化测试工作流中的实战手册。我们不仅要学会怎么生成那个花花绿绿的覆盖率报告更要理解覆盖率数据背后的含义知道如何利用它来指导我们写出更健壮、更有效的测试最终目标是让自动化测试真正成为产品质量的可靠守护者而不是自我感动的“绿色通过率”。简单来说这个项目能帮你做三件事第一给你的 Playwright 测试项目装上“眼睛”让它能看清自己到底测试了哪些代码第二生成直观的可视化报告让你一眼就能找到测试的薄弱环节第三基于数据驱动测试策略的优化把有限的测试精力投入到最需要覆盖的地方。无论你是测试开发工程师、全栈开发者还是对质量有要求的团队负责人这套方法都能让你的测试工作从“凭感觉”走向“看数据”实现质的飞跃。2. 核心工具链与方案选型解析在 Playwright 的世界里收集测试覆盖率并不是一个开箱即用的功能需要我们组合几个工具。市面上方案不少但经过实际踩坑和对比我推荐下面这套稳定、高效且与现代前端工程化契合度最高的组合拳。这套方案的核心思想是在测试运行时通过代码插桩来收集数据最后统一生成报告。2.1 为什么是 Istanbul (nyc) babel-plugin-istanbul首先我们需要一个覆盖率收集和报告生成工具。Istanbul现在通常通过它的命令行工具nyc来使用是 JavaScript 生态中事实上的标准社区成熟报告格式丰富HTML、LCOV、JSON等并且能与各种测试运行器和构建工具无缝集成。但 Istanbul 自己不会“侵入”你的源代码。这时就需要babel-plugin-istanbul出场了。它的作用是在代码被测试执行前通过 Babel 转译的过程悄悄地在每一行、每一个函数、每一个分支语句上插入一些“计数器”。当你的 Playwright 测试在浏览器中运行被测应用代码时这些计数器就会被触发记录下代码的执行路径。这是一种“插桩”技术。为什么不直接用 Playwright 的 Coverage APIPlaywright 确实提供了page.coverageAPI 来收集 JS 和 CSS 覆盖率。但它收集的是“资源级”的覆盖率即哪些 JS/CSS 文件被加载了以及文件中有多少字节被执行了。这对于优化资源加载很有用但对于我们评估“业务逻辑代码是否被测试到”这个目标来说粒度太粗了。我们需要的是“行级”、“分支级”的覆盖率这必须通过源代码插桩来实现。选型考量总结精度要求我们需要行/分支/函数级别的细粒度覆盖率因此源代码插桩是唯一选择。生态兼容nyc和babel-plugin-istanbul是 React、Vue、Next.js 等主流前端框架的常见配置接入成本低。报告能力nyc能生成非常详尽的 HTML 报告可以直观地看到哪些行被覆盖绿色、哪些行没被覆盖红色以及哪些是条件分支黄色。2.2 Playwright Test Runner 的角色在本项目中Playwright Test 不仅是执行测试的工具更是整个流程的组织者和驱动者。我们将利用它的fixture、hook如beforeEachafterEach和配置文件playwright.config.ts来编排覆盖率数据的收集时机。具体来说我们会在测试开始前启动一个已经插桩好的应用服务器。在测试执行过程中Playwright 驱动浏览器访问该服务器并运行测试用例。在测试结束后从浏览器上下文中提取收集到的覆盖率原始数据并写入到本地文件。Playwright Test 的稳定性、并行测试能力以及对多浏览器的支持保证了覆盖率收集过程可以像普通测试一样在 CI/CD 流水线中大规模、可靠地运行。2.3 构建工具链的整合Vite/Webpack 的配合你的前端项目大概率使用了 Vite 或 Webpack 进行构建。我们需要让覆盖率插桩与开发/构建流程协同工作。通常我们会为测试环境创建一个特定的构建配置。开发模式在运行测试时我们通常不希望启动完整的生产构建那样太慢。我们可以利用 Vite 的开发服务器并通过配置让 Babel 插件只在测试环境下启用插桩。这能实现最快的测试反馈循环。CI/CD 模式在流水线中我们可能会先构建一个插桩后的生产版本再针对这个版本运行测试并收集覆盖率。这更接近真实场景但耗时更长。在演示项目中我们会聚焦于开发/测试模式下的配置因为这是最常用、迭代最快的场景。我们会通过环境变量如process.env.NODE_ENV test来控制babel-plugin-istanbul的启用与禁用。3. 项目初始化与基础环境搭建让我们开始动手。假设我们有一个基于 Vite React 的前端项目并且已经使用 Playwright 编写了一些基础测试。如果没有请先初始化。3.1 安装依赖首先进入你的项目根目录安装必要的依赖。# 确保已有 Playwright 测试相关依赖 npm init playwrightlatest --yes # 如果尚未安装 Playwright Test # 安装覆盖率工具链 npm install --save-dev nyc babel-plugin-istanbul istanbuljs/nyc-config-babelnyc命令行工具用于包装测试命令、收集和报告覆盖率。babel-plugin-istanbulBabel 插件负责代码插桩。istanbuljs/nyc-config-babel为 nyc 提供与 Babel 配合的良好默认配置。3.2 配置 Babel 以启用插桩如果你的项目使用了babel.config.js或.babelrc我们需要修改它让它在测试环境下应用babel-plugin-istanbul。创建或修改babel.config.js// babel.config.js module.exports (api) { // 缓存配置提升性能 api.cache.using(() process.env.NODE_ENV); const isTest api.env(test); return { presets: [ // 你的其他 preset例如 babel/preset-react, babel/preset-typescript [babel/preset-env, { targets: { node: current } }], ], plugins: [ // ... 你的其他插件 // 仅在测试环境下启用覆盖率插桩插件 ...(isTest ? [istanbul] : []), ], }; };关键点说明api.env(test)这是 Babel 提供的环境判断 API。当我们在package.json中设置NODE_ENVtest来运行测试时这个条件为真。istanbul这是babel-plugin-istanbul的简写。我们只在测试时启用它避免插桩代码被意外打包到生产环境中影响性能和代码体积。注意如果你的项目使用 Vite 且没有显式配置 Babel很多现代项目直接用 Vite 的构建能力你可能需要通过vitejs/plugin-react的babel选项来传入此配置或者使用vite-plugin-istanbul这样的专用插件。为了概念清晰本指南采用基于 Babel 的通用方案。如果你的项目是纯 Vite搜索并配置vite-plugin-istanbul是更直接的选择。3.3 配置 NYC (.nycrc)在项目根目录创建.nycrc文件用来配置nyc的行为。这个配置告诉nyc如何查找插桩后的代码、要收集哪些文件、以及如何生成报告。{ extends: istanbuljs/nyc-config-babel, all: true, include: [src/**/*.js, src/**/*.jsx, src/**/*.ts, src/**/*.tsx], exclude: [**/*.spec.js, **/*.test.js, **/*.stories.js, src/**/index.js], reporter: [html, text, lcov], check-coverage: false, temp-dir: .nyc_output }extends继承一个共享配置这里用了针对 Babel 的配置。all: 设置为true意味着即使某些文件从未被require或import也会被纳入覆盖率计算范围。这有助于发现完全未被触及的“死代码”。include指定需要计算覆盖率的源代码文件路径模式。exclude排除测试文件本身、故事书文件或入口文件避免它们影响覆盖率统计。reporter指定报告格式。html生成可浏览的网页报告text在终端输出简要摘要lcov生成lcov.info文件可用于与 CI 工具如 Codecov, Coveralls集成。check-coverage设为false我们先不设置强制性的覆盖率阈值等流程跑通后再调整。temp-dir指定原始覆盖率数据JSON 格式的临时输出目录。4. 改造 Playwright 配置以收集覆盖率数据这是最核心的一步。我们需要修改playwright.config.ts让它在测试生命周期中完成三件事1. 启动插桩后的应用2. 在测试结束后收集数据3. 将数据保存到nyc能读取的位置。4.1 创建自定义 Fixture 来启动测试服务器我们不会直接使用playwright test --ui或访问线上地址而是要在测试内部启动一个本地开发服务器这个服务器提供的是经过 Babel 插桩后的代码。首先在项目根目录创建一个文件tests/coverage-server.fixture.ts// tests/coverage-server.fixture.ts import { test as base, expect } from playwright/test; import { exec } from child_process; import { promisify } from util; const execAsync promisify(exec); // 声明 Fixture 的类型 export type CoverageServerFixtures { coverageServerPort: number; }; // 扩展基础的 test fixture export const test base.extendCoverageServerFixtures({ // 提供一个固定的端口号也可以动态生成 coverageServerPort: [async ({}, use) { await use(3001); // 使用 3001 端口启动测试服务器 }, { scope: worker }], // scope 为 worker 确保所有 worker 复用同一个端口配置 // 覆盖 page fixture使其自动导航到我们的覆盖率服务器 page: async ({ coverageServerPort, browser }, use) { // 启动一个子进程来运行 Vite 开发服务器并设置 NODE_ENVtest // 注意这里假设你的 package.json 中 vite 命令能启动开发服务器 const serverProcess execAsync(NODE_ENVtest vite --port ${coverageServerPort}, { cwd: process.cwd(), }).catch(e console.error(Server might already be running or failed:, e)); // 忽略重复启动错误 // 给服务器一点时间启动 await new Promise(resolve setTimeout(resolve, 3000)); // 创建新的页面上下文并设置 baseURL const context await browser.newContext({ baseURL: http://localhost:${coverageServerPort}, }); const page await context.newPage(); // 将 page 提供给测试用例使用 await use(page); // 测试结束后关闭上下文 await context.close(); // 这里我们通常不主动杀死服务器进程因为它是 worker 级别的可能会被其他测试复用。 // 更好的做法是在 globalTeardown 中处理。这里为了演示简化了。 }, }); export { expect };这个 Fixture 做了什么它定义了一个coverageServerPort固定为 3001。它重写了默认的pagefixture。在创建 page 之前它尝试在端口 3001 上启动一个设置了NODE_ENVtest的 Vite 开发服务器。这个环境变量会触发我们之前配置的 Babel 插件进行代码插桩。它创建的新页面上下文browser.newContext的baseURL指向了这个本地服务器这样测试中的page.goto(/)就会访问我们插桩后的应用。4.2 修改主配置文件并注入收集逻辑现在修改playwright.config.ts使用我们自定义的 fixture并添加收集覆盖率的逻辑。// playwright.config.ts import { defineConfig } from playwright/test; import { test } from ./tests/coverage-server.fixture; // 导入自定义的 test fixture export default defineConfig({ // 使用我们自定义的、带覆盖率服务器的 test test, // ... 其他原有配置 (timeout, retries, workers等) use: { // 全局的截图、录像等配置可以保留在这里 trace: on-first-retry, }, // 全局的 Setup 和 Teardown用于处理覆盖率数据的收集和合并 globalSetup: require.resolve(./tests/global-setup), globalTeardown: require.resolve(./tests/global-teardown), });接下来创建全局的 Setup 和 Teardown 文件。tests/global-setup.ts主要做清理工作。// tests/global-setup.ts import fs from fs-extra; import path from path; const nycOutputDir path.join(process.cwd(), .nyc_output); async function globalSetup() { // 每次运行测试前清空之前的覆盖率数据目录避免旧数据污染 await fs.remove(nycOutputDir); await fs.ensureDir(nycOutputDir); console.log(Cleaned up previous coverage data.); } export default globalSetup;tests/global-teardown.ts这是关键它在所有测试 worker 结束后运行负责触发覆盖率报告的生成。// tests/global-teardown.ts import { exec } from child_process; import { promisify } from util; const execAsync promisify(exec); async function globalTeardown() { console.log(All tests finished. Generating coverage report...); try { // 使用 nyc 命令生成报告 // nyc report 会读取 .nyc_output 目录下的数据并生成我们在 .nycrc 中配置的报告 const { stdout, stderr } await execAsync(npx nyc report); console.log(Coverage report generated successfully.); if (stderr) { console.warn(nyc stderr:, stderr); } } catch (error) { console.error(Failed to generate coverage report:, error); process.exit(1); // 如果报告生成失败视作测试运行失败 } } export default globalTeardown;4.3 在测试中收集窗口覆盖率数据上面的配置启动了插桩服务器并安排了报告生成但还没告诉 Playwright 如何从每个测试页面中提取覆盖率数据。我们需要在每个测试执行后从浏览器中获取window.__coverage__对象这是babel-plugin-istanbul注入的全局变量并保存下来。我们可以在自定义的pagefixture 中或者通过一个额外的 fixture 来实现。这里我们在自定义的pagefixture 完成后添加收集逻辑。修改之前的tests/coverage-server.fixture.ts中的pagefixture// 在 tests/coverage-server.fixture.ts 中更新 page fixture page: async ({ coverageServerPort, browser }, use) { const serverProcess execAsync(NODE_ENVtest vite --port ${coverageServerPort}, { cwd: process.cwd(), }).catch(e console.error(Server might already be running or failed:, e)); await new Promise(resolve setTimeout(resolve, 3000)); const context await browser.newContext({ baseURL: http://localhost:${coverageServerPort}, }); const page await context.newPage(); await use(page); // --- 新增测试结束后收集覆盖率数据 --- if (process.env.COVERAGE true) { // 可以通过环境变量控制是否收集 const coverage await page.evaluate(() { // ts-ignore - __coverage__ 是 istanbul 注入的 return window.__coverage__; }); if (coverage) { const testInfo (page as any).testInfo; // 获取当前测试信息 const testName testInfo?.titlePath?.join( ) || unknown-test; const safeTestName testName.replace(/[^a-z0-9]/gi, _).toLowerCase(); const coveragePath path.join(process.cwd(), .nyc_output, coverage-${safeTestName}-${Date.now()}.json); await fs.writeJson(coveragePath, coverage); console.log(Coverage data saved for: ${testName}); } } // --- 收集结束 --- await context.close(); },关键点解析page.evaluate(() window.__coverage__)这是在浏览器上下文中执行 JavaScript获取插桩代码收集到的覆盖率对象。(page as any).testInfo我们通过一个非公开的 API在 Playwright 类型中可能未定义来获取当前测试的名称。更稳健的做法是使用 Playwright 的test.info()API但这需要在测试函数内部调用。这里为了简化演示了在 fixture 中获取的思路。在实际项目中你可能需要在每个测试的afterEachhook 中显式调用一个收集函数。我们将每个测试的覆盖率数据单独保存为一个 JSON 文件在.nyc_output目录下。nyc report命令会自动合并所有这些文件。实操心得在实际项目中从pagefixture 的use回调之后收集覆盖率有时会因页面过早关闭而失败。更可靠的做法是在每个测试文件的顶部定义一个afterEachhook或者创建一个名为collectCoverage的 fixture在测试中显式调用。虽然稍显繁琐但稳定性极高。例如// 在测试文件中 import { test, expect } from ../tests/coverage-server.fixture; test.afterEach(async ({ page }) { const coverage await page.evaluate(() (window as any).__coverage__); if (coverage) { // ... 保存 coverage 到文件 ... } }); test(my test, async ({ page }) { await page.goto(/my-page); // ... 测试逻辑 ... });5. 编写测试并查看覆盖率报告环境配置好了现在我们来写一个简单的测试看看整个流程如何运作。5.1 创建被测组件与测试用例假设我们有一个简单的计数器组件src/components/Counter.jsx// src/components/Counter.jsx import React, { useState } from react; function Counter() { const [count, setCount] useState(0); const [isEven, setIsEven] useState(true); const increment () { const newCount count 1; setCount(newCount); setIsEven(newCount % 2 0); // 分支逻辑判断奇偶 }; const decrement () { if (count 0) { // 分支逻辑防止负数 const newCount count - 1; setCount(newCount); setIsEven(newCount % 2 0); } }; return ( div h1>// tests/counter.spec.ts import { test, expect } from ./coverage-server.fixture; // 使用自定义 fixture test.describe(Counter Component, () { test.beforeEach(async ({ page }) { // 假设你的路由能渲染 Counter 组件或者直接导航到包含它的页面 await page.goto(/counter); }); test(should increment count and update even/odd status, async ({ page }) { await expect(page.getByTestId(count-display)).toHaveText(Count: 0); await expect(page.getByTestId(even-odd-display)).toHaveText(Is even: Yes); await page.getByTestId(increment-btn).click(); await expect(page.getByTestId(count-display)).toHaveText(Count: 1); await expect(page.getByTestId(even-odd-display)).toHaveText(Is even: No); }); test(should not decrement below zero, async ({ page }) { await expect(page.getByTestId(count-display)).toHaveText(Count: 0); // 点击递减按钮此时 count0应该不执行递减逻辑 await page.getByTestId(decrement-btn).click(); // 断言显示仍为 0 await expect(page.getByTestId(count-display)).toHaveText(Count: 0); // 这个测试用例没有覆盖到 decrement 函数中 count0 的分支 }); });5.2 运行测试并生成报告现在运行测试命令。我们需要设置环境变量来启用覆盖率收集并使用我们配置了自定义 fixture 的 Playwright。在package.json中添加脚本{ scripts: { test:coverage: COVERAGEtrue playwright test --configplaywright.config.ts, coverage:report: nyc report } }然后运行npm run test:coverage这个命令会设置COVERAGEtrue。启动 Playwright 测试使用我们修改过的 config。每个测试结束后会将window.__coverage__数据保存到.nyc_output。所有测试完成后globalTeardown会执行nyc report生成最终报告。5.3 解读覆盖率报告运行完成后打开coverage/index.html文件这是nyc默认生成的 HTML 报告位置。你会看到一个类似这样的界面摘要页显示总体的行覆盖率Line Coverage、语句覆盖率Statement Coverage、分支覆盖率Branch Coverage和函数覆盖率Function Coverage的百分比。文件列表点击src/components/Counter.jsx你会进入文件详情页。在Counter.jsx的详情页代码会被高亮绿色该行代码被测试执行到了。红色该行代码从未被执行。黄色该行包含条件分支如if、三元运算符且分支未被完全覆盖。鼠标悬停会显示“Branch X of Y not covered”。分析我们的测试报告increment函数和相关的 UI 交互被第一个测试用例覆盖了相关行应该是绿色的。decrement函数中的if (count 0)分支由于我们的第二个测试用例初始 count 为 0没有进入if内部所以这个if行会是黄色的并且if块内的代码行setCount,setIsEven会是红色的。这直观地告诉我们现有的测试没有覆盖到“从正数递减”这个业务场景。这就是覆盖率报告的价值——它用数据指出了测试的盲区。6. 高级技巧与最佳实践掌握了基础流程后下面这些技巧能让你的覆盖率实践更上一层楼。6.1 设置覆盖率阈值与 CI 集成不能让覆盖率只停留在“看看”的阶段。我们可以通过nyc的check-coverage功能在 CI 流水线中设置质量关卡。修改.nycrc{ // ... 其他配置 check-coverage: true, branches: 80, lines: 85, functions: 85, statements: 85 }这样配置后运行nyc report时如果任何一项覆盖率指标低于阈值命令就会以非零状态码退出导致 CI 构建失败。这能强制团队维持一定的测试质量标准。在 CI 脚本中如 GitHub Actions- name: Run Tests with Coverage run: npm run test:coverage - name: Check Coverage Thresholds run: npx nyc check-coverage # 或者直接在上一步的 test:coverage 脚本中包含报告生成和检查6.2 处理源代码映射Source Maps如果你的项目使用 TypeScript 或经过压缩生成的覆盖率报告可能指向编译后的代码难以阅读。需要确保nyc能正确处理 Source Maps。首先确保你的构建工具如 Vite、Webpack在生产构建时生成 Source Maps对于测试构建也需要。然后在.nycrc中启用相关配置{ // ... 其他配置 sourceMap: true, instrument: false // 如果使用 babel-plugin-istanbul 插桩这里设为 false }并且需要安装source-map-supportnpm install --save-dev source-map-support在playwright.config.ts的globalSetup或测试运行入口处引入import source-map-support/register;这样HTML 报告中的代码就能正确映射回你的原始源代码文件了。6.3 排除无需覆盖的代码不是所有代码都需要高覆盖率。比如配置文件、第三方库的垫片、样式文件、或者某些纯展示型组件。盲目追求 100% 覆盖率是性价比很低的行为。在.nycrc的exclude数组中仔细配置exclude: [ **/*.spec.*, **/*.test.*, **/*.stories.*, **/*.config.*, **/types/**, **/dist/**, **/build/**, **/coverage/**, src/main.tsx, // 应用入口通常逻辑简单 src/vite-env.d.ts ]对于代码文件中的特定行可以使用 Istanbul 的特殊注释来忽略/* istanbul ignore next */ // 忽略下一行 /* istanbul ignore if */ // 忽略下一个 if 分支 /* istanbul ignore file */ // 忽略整个文件6.4 并行测试下的覆盖率数据合并Playwright 默认会并行运行测试通过workers配置。每个 worker 进程都会生成自己的覆盖率数据文件。nyc的report命令能自动合并.nyc_output目录下的所有*.json文件所以我们的方案天然支持并行。但要确保每个 worker 写入的文件名是唯一的我们用了时间戳和测试名。globalTeardown在所有 worker 结束后运行Playwright 的globalTeardown正是如此。6.5 与 VS Code 和 MCP AI 辅助工具结合从你提供的热词中看到“visual studio code绑定cline使用playwright mcpai辅助功能”这指向了利用 AI 辅助编写测试。覆盖率报告可以反向指导 AI。工作流建议运行现有测试生成覆盖率报告。打开 HTML 报告找到红色未覆盖的复杂业务逻辑代码块。将这些代码块作为上下文提供给 VS Code 中的 AI 编程助手如 Cline、Copilot并提示“为以下这段尚未被测试覆盖的 React 组件代码编写一个 Playwright 测试用例覆盖其主要分支和边缘情况。”AI 生成的测试代码需要你进行审查和调整然后加入测试套件。再次运行测试观察覆盖率变化形成“分析-生成-验证”的闭环。这种“覆盖率报告驱动 AI 辅助补全”的模式能极大提升编写针对性测试用例的效率。7. 常见问题排查与实战心得在实际搭建过程中你几乎一定会遇到下面这些问题。这里是我的踩坑记录和解决方案。7.1 问题window.__coverage__是undefined这是最常见的问题意味着插桩没有成功。排查步骤确认环境变量确保运行测试时NODE_ENVtest。在启动测试服务器的命令中检查。检查 Babel 配置在测试环境下babel-plugin-istanbul是否被正确添加到 plugins 数组可以通过在 Babel 配置中临时加一个console.log来调试。检查源代码在浏览器开发者工具的 Console 中直接输入window.__coverage__看看。如果为undefined说明页面加载的 JS 文件没有被插桩。可能是你的构建工具如 Vite在开发模式下使用了其他转换管道绕过了 Babel。对于 Vite 项目强烈建议使用vite-plugin-istanbul。服务器是否正确启动确保你的自定义 fixture 成功启动了开发服务器并且页面确实导航到了这个本地服务器地址而不是别的地址。7.2 问题覆盖率数据为零或极低测试运行了报告生成了但覆盖率全是 0% 或很低。排查步骤确认测试是否真的执行了应用代码你的测试是不是只做了page.goto()然后断言了一些静态文本如果测试没有触发任何事件点击、输入等业务逻辑代码就不会执行。确保测试模拟了用户交互。检查include路径.nycrc中的include模式是否匹配了你的源代码文件比如你的文件是.tsx但配置里只写了*.js。检查文件是否被排除exclude列表是否意外排除了你的业务代码目录查看原始数据去.nyc_output目录下打开一个 JSON 文件看看。里面应该有具体的文件路径和覆盖数据。如果文件是空的或只包含测试文件说明收集环节有问题。7.3 问题报告中的行号对不上或指向奇怪的文件这通常是 Source Map 配置问题。解决方案确保测试时构建生成了 Source Mapvite build --mode test --sourcemap。确认.nycrc中sourceMap: true。确保source-map-support已安装并在入口处注册。7.4 问题并行测试时数据丢失或报告不准解决方案文件名冲突确保每个保存覆盖率 JSON 文件的文件名是唯一的例如包含process.pid进程ID或testInfo.testId。写入时机确保数据是在测试真正结束后收集的。如果在page.close()或context.close()之后才尝试page.evaluate会因为页面上下文已销毁而失败。这就是为什么推荐在afterEachhook 中收集而不是在 fixture 的 teardown 逻辑中。全局清理globalSetup中一定要清空.nyc_output目录避免上次运行的残留数据影响本次结果。7.5 实战心得覆盖率不是银弹如何正确使用不要盲目追求高百分比100% 覆盖率很美但成本极高。重点覆盖核心业务逻辑、复杂分支和容易出错的代码。工具函数、简单的 UI 渲染可以适当放宽要求。关注“分支覆盖率”和“函数覆盖率”行覆盖率Line Coverage最容易提升但分支覆盖率Branch Coverage更能反映测试的完备性。一个if-else语句两行都执行了行覆盖率100%但可能只覆盖了if为真的情况分支覆盖率只有50%。覆盖率是发现漏洞的地图不是质量合格的奖章覆盖率告诉你“哪里没测到”但无法告诉你“测得好不好”。一个断言都没有的测试即使覆盖了代码也毫无价值。覆盖率必须与有意义的断言相结合。将覆盖率检查作为 CI 的强制门禁设置合理的、逐步提升的阈值如从 60% 开始让覆盖率成为代码合并前必须通过的检查项这样才能持续改进测试文化。定期审查低覆盖率模块在团队周会或代码评审中定期查看覆盖率报告针对持续低覆盖率的模块进行讨论是技术债还是测试用例缺失制定改进计划。