前端自动化测试项目复盘:从零覆盖到80%的单测与E2E建设经验
前端自动化测试项目复盘从零覆盖到80%的单测与E2E建设经验一、为什么过去没写测试一个运营2年的Vue3中后台项目测试覆盖率约3%只有2个utils函数的测试。过去不写测试的原因项目迭代太快写测试拖慢开发UI变化频繁测试维护成本高手动测试就够了直到一次上线事故修改了权限验证逻辑3天后才发现某个子页面的权限检查失效了——因为没人手动测过那个不常用的页面。损失的3天里用户可能访问了不该访问的数据。测试不是为现在的代码写的是为未来修改代码的人写的。二、四阶段渐进式建设阶段一测试基础设施2周选择VitestVite原生、速度快 vue/test-utils Testing Library// vitest.config.ts import { defineConfig } from vitest/config; import vue from vitejs/plugin-vue; export default defineConfig({ plugins: [vue()], test: { environment: jsdom, globals: true, coverage: { provider: v8, reporter: [text, json, html], thresholds: { statements: 80, branches: 75, functions: 80, lines: 80, }, }, }, });CI集成——每次PR自动跑测试。阶段二工具函数测试2周——投入产出比最高优先测试纯逻辑函数不需要mock DOM、收益最大// utils/format.test.ts import { describe, it, expect } from vitest; import { formatPrice, formatDateRelative } from ./format; describe(formatPrice, () { it(正常价格格式化, () { expect(formatPrice(1234.56)).toBe(¥1,234.56); }); it(null/undefined返回--, () { expect(formatPrice(null)).toBe(--); expect(formatPrice(undefined)).toBe(--); }); it(0显示为0, () { expect(formatPrice(0)).toBe(¥0.00); }); it(负数处理, () { expect(formatPrice(-99)).toBe(-¥99.00); }); });这个阶段的2400行测试代码覆盖率最高utils层达到95%发现了3个隐藏Bug。阶段三Vue组件测试4周重点测试组件的用户交互不测试实现细节// components/SearchForm.test.ts import { render, screen, fireEvent } from testing-library/vue; import userEvent from testing-library/user-event; import SearchForm from ./SearchForm.vue; describe(SearchForm, () { it(输入关键词后点击搜索触发search事件, async () { const { emitted } render(SearchForm); const input screen.getByPlaceholderText(搜索...); await userEvent.type(input, 测试关键词); const button screen.getByRole(button, { name: 搜索 }); await userEvent.click(button); expect(emitted().search[0]).toEqual([{ keyword: 测试关键词 }]); }); it(空输入点击搜索不触发事件, async () { const { emitted } render(SearchForm); const button screen.getByRole(button, { name: 搜索 }); await userEvent.click(button); expect(emitted().search).toBeUndefined(); }); it(表单校验——超过100字符提示, async () { render(SearchForm); const input screen.getByPlaceholderText(搜索...); await userEvent.type(input, a.repeat(101)); expect(screen.getByText(搜索关键词不能超过100个字符)).toBeTruthy(); }); });组件测试原则测试用户行为点击、输入、看到什么不测试实现data属性、method调用。阶段四E2E测试4周选择Playwright——比Cypress更快、支持多浏览器、API更简洁// e2e/login.spec.ts import { test, expect } from playwright/test; test.describe(登录流程, () { test(正常登录, async ({ page }) { await page.goto(/login); await page.fill([data-testidusername], admin); await page.fill([data-testidpassword], password123); await page.click([data-testidlogin-btn]); // 验证跳转到首页 await expect(page).toHaveURL(/dashboard); }); test(错误密码提示, async ({ page }) { await page.goto(/login); await page.fill([data-testidusername], admin); await page.fill([data-testidpassword], wrong); await page.click([data-testidlogin-btn]); await expect(page.locator(.error-message)) .toContainText(用户名或密码错误); }); });三、测试覆盖率从3%到75%的实际效果指标建设前建设后代码覆盖率3%75%线上Bug率月均12个4个重构信心低不敢改高跑测试验证CI失败后的修复时间无CI15分钟测试运行时间—单测45s E2E 3min线上Bug率从12降到4下降67%。更重要的隐性收益重构时不再心惊胆战——改完代码跑一下测试绿灯就敢上线。四、测试维护的代价指标数据测试代码总量约8000行测试代码/业务代码比0.6:1每轮迭代的测试维护时间约15%测试首次通过率85%15%的测试维护时间投入换来了67%的Bug减少——ROI为正。测试代码/业务代码比0.6:1低于行业常见的1:1说明当前测试仍偏向关键路径而非全覆盖。测试分类策略核心业务路径登录、支付、权限→ E2E测试。数据变换逻辑格式化、计算、校验→ 单元测试。用户交互流程表单、搜索、列表操作→ 组件测试。五、总结从零到75%覆盖率的测试建设工具函数优先测——纯逻辑、投入产出比最高组件测试关注用户行为不测试实现细节——实现改了测试不改E2E覆盖核心业务流程5-10条——不是所有页面都需要E2ECI中强制测试通过是覆盖率不退化的保障15%的迭代时间用于测试维护是合理且必要的投入最大的教训不要在所有代码都写完了之后才补测试。正确的方式是边写功能边写测试——每个新功能至少带一个测试。从零覆盖到75%用了12周的主要原因是前8周在给旧代码补测试最痛苦的部分。如果从项目第一天起就保持测试习惯达到75%覆盖率只需要维持而非追赶。