前端测试的黄金比例

「要不要写测试」已经没有争议了。真正的问题是——写多少?写在哪一层?

测试金字塔

经典的测试分层理论,前端同样适用:

        /\
       /E2E\        少量,覆盖核心流程
      /------\
     /集成测试\      中等量,验证模块协作
    /----------\
   /  单元测试  \    大量,验证纯逻辑
  /--------------\

[!TIP] 建议从集成测试开始,它们的投入产出比最高。纯单元测试和 E2E 测试作为补充。

[!WARNING] 不要为了 100% 覆盖率而测试。覆盖所有分支的测试可能比没有测试更糟糕——它们让重构变得极其痛苦。

测试覆盖率有上限

测试覆盖率可以用数学描述。假设所有代码路径均匀分布,实际能覆盖的场景符合指数增长规律:

Coverage(n)=1eλn\text{Coverage}(n) = 1 - e^{-\lambda n}

其中 λ\lambda 是每个测试用例的平均覆盖密度。这意味着前 20% 的测试用例往往覆盖了 80% 的代码路径。

[!NOTE] 一般项目把覆盖率目标定在 70-80% 比较合理。关键模块(支付、鉴权)可以设得更高。

比例建议

测试类型占比适用场景
单元测试60%工具函数、Hooks、状态管理
集成测试30%表单、列表交互、数据流
E2E10%注册、下单、支付等核心流程

不是越底层越好——测试 UI 实现细节(如某个 div 是否渲染)的单元测试脆弱易碎,性价比极低。

单元测试:聚焦纯逻辑

// utils.ts
export function formatPrice(price: number, currency = 'CNY'): string {
  return new Intl.NumberFormat('zh-CN', { style: 'currency', currency }).format(price);
}

// utils.test.ts
import { describe, it, expect } from 'vitest';

describe('formatPrice', () => {
  it('formats CNY correctly', () => {
    expect(formatPrice(99.9)).toBe('¥99.90');
  });

  it('handles zero', () => {
    expect(formatPrice(0)).toBe('¥0.00');
  });

  it('formats USD', () => {
    expect(formatPrice(10, 'USD')).toBe('US$10.00');
  });
});

集成测试:验证组件行为

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect } from 'vitest';

describe('SearchInput', () => {
  it('calls onSearch after typing and clicking button', async () => {
    const user = userEvent.setup();
    const onSearch = vi.fn();

    render(<SearchInput onSearch={onSearch} />);

    await user.type(screen.getByPlaceholderText('搜索...'), 'react');
    await user.click(screen.getByRole('button', { name: '搜索' }));

    expect(onSearch).toHaveBeenCalledWith('react');
  });
});

Testing Library 哲学:模拟用户行为,而非测试内部 state。你的测试不会因为重构实现而挂掉。

E2E:保护核心路径

// e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';

test('complete checkout flow', async ({ page }) => {
  await page.goto('/products/1');
  await page.click('button:has-text("加入购物车")');
  await page.goto('/cart');
  await page.click('button:has-text("结算")');

  await page.fill('input[name="address"]', '北京市朝阳区');
  await page.click('button:has-text("确认下单")');

  await expect(page.locator('.order-success')).toBeVisible();
});

只测最重要的 3-5 个流程就够了。

工具推荐

场景推荐工具
单元/集成测试Vitest + Testing Library
E2EPlaywright
视觉回归Chromatic / Percy
覆盖率Istanbul (V8 coverage)

测试不是负担,是让你放心重构的安全网。从 0 到 1 先写几个关键用例,感受到收益后再逐步扩大覆盖率。