React 学习站
React›前沿与实践›进阶

测试策略

进阶前沿与实践

一句话定义

React 测试按「金字塔」分层:纯逻辑用单元测试(Vitest)、组件按用户可见行为测试(Testing Library)、关键路径做端到端(Playwright)——工具选型的原则是「测用户看到的,不测实现细节」。

为什么重要

React 组件最大的测试陷阱是「测实现」:mock 到 useState、断言 className 组合的测试在重构时全线崩红,却不拦任何真 bug。这一篇给出可持续的分层策略——它决定你的测试资产三个月后是护城河还是负债。

前置知识

  • kp-015 自定义 Hook 可脱离组件测试。
  • 基本的测试概念(describe/it/expect、mock)。

核心概念

  • 测试金字塔:大量单元(reducer/工具/Hook)→ 中量组件测试 → 少量 E2E。
  • 用户视角查询:getByRole('button', { name: '提交' }) 优于 getByTestId,因为角色查询同时验证了可访问性。
  • 不测实现细节:不断言「state 更新了几次」,只断言「用户看到什么、点击后看到什么」。
  • Mock 边界:网络层用 MSW(拦截 fetch,提供真实响应),组件内部逻辑不 mock。
  • 钩子测试:renderHook + act,或把 Hook 逻辑下沉为纯函数优先测试。
  • 性能回归:Profiler 断言 / 长列表渲染时间预算进 CI(进阶项)。

原理 / 机制

分层示例:一个 todo 应用

jsx// 1. 单元层:reducer 是纯函数,测试零成本(kp-016 的复利)
test('add 去重合并数量', () => {
  const state = cartReducer({ items: [{ id: 1, qty: 1 }] },
                            { type: 'add', item: { id: 1 } });
  expect(state.items).toEqual([{ id: 1, qty: 2 }]);
});

// 2. 组件层:用户行为视角(Testing Library)
test('勾选复选框后提交按钮可用', async () => {
  render(<Signup />);
  const user = userEvent.setup();
  await user.click(screen.getByRole('checkbox'));
  expect(screen.getByRole('button', { name: /提交/ })).toBeEnabled();
});

// 3. E2E 层:Playwright 走真实浏览器
test('登录后添加书签', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('邮箱').fill('a@b.c');
  await page.getByLabel('密码').fill('secret');
  await page.getByRole('button', { name: '登录' }).click();
  await page.getByPlaceholder('添加书签').fill('https://react.dev');
  await page.keyboard.press('Enter');
  await expect(page.getByRole('link', { name: 'react.dev' })).toBeVisible();
});

MSW 处理数据请求(避免 mock 整个 fetch 的脆断言):

jsximport { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

const server = setupServer(
  http.get('/api/search', ({ request }) => {
    const q = new URL(request.url).searchParams.get('q');
    return HttpResponse.json([{ id: 1, name: `结果-${q}` }]);
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());   // 单测间互不污染
afterAll(() => server.close());

自定义 Hook 测试(kp-015 的可测性回报):

jsxtest('useDebouncedValue 延迟生效', async () => {
  const { result, rerender } = renderHook(({ v }) => useDebouncedValue(v, 100),
                                          { initialProps: { v: 'a' } });
  rerender({ v: 'ab' });
  expect(result.current).toBe('a');         // 未到延迟
  await act(() => advanceBy(150));
  expect(result.current).toBe('ab');
});

直观类比

测试分层像质检体系:单元测试是材料检验(每颗螺丝合格);组件测试是部件抽检(车门关得上、按得响);E2E 是整车路试(上高速跑一圈)。只做路试,坏了不知道哪颗螺丝(定位慢、修复贵);只测螺丝,整车可能装反(覆盖假象)。用户视角查询是「让盲人质检员来摸」:他只能靠角色和名字找到东西——所以你的 aria 无障碍顺带被检验了。

实例 / 案例

一个中型项目的分层配比参考:

textVitest        单元: reducer/工具/纯函数 Hook   ~200 个,<2s
Testing Lib   组件: 关键交互组件               ~60 个
MSW           拦截: 所有网络边界
Playwright    E2E: 登录/下单/支付 3 条主路径    ~8 条
CI            PR 必过单测+组件; main 加跑 E2E; Lighthouse 预算

配置三角(Vite 生态):

js// vitest.config.js
import react from '@vitejs/plugin-react';
export default {
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: './test/setup.js',     // 引入 jest-dom 断言 + MSW
  },
};

常见误区

  • 断言实现细节:expect(component.state.count).toBe(3) / 断言 className 拼接——重构即红,维护即弃。
  • 快照测试滥用:大快照「看都不看就 update」;快照只适合小型稳定结构(如渲染的图标 SVG)。
  • mock 到失控:mock 掉被测系统本身(mock useState、mock 组件内部);mock 应止步于网络/时钟/存储等「边界」。
  • E2E 当第一道防线:慢、脆、贵;只留给「业务断了会流血」的路径。
  • 异步断言忘 await:expect(getByText(...)) 在用户点击后立即断言会偶发失败;统一用 findBy/waitFor(act 警告的同源问题)。
  • RSC 场景照搬组件测试:Server Component 的取数逻辑应作为普通 async 函数测试,浏览器环境测不了它(与 kp-026 呼应)。

自测题

  1. 「测用户看到的,不测实现」——各写一个正确与错误断言的例子。
  2. 为什么 getByRole 优于 getByTestId?它的副作用收益是什么?
  3. MSW 相比 vi.mock('./api') 的优势是什么?beforeEach 里 resetHandlers 防什么?
  4. 给你的 todo 项目设计测试金字塔:列出每层测什么、各几条。
  5. 「测试在重构时全线崩红」说明测试的什么问题?如何预防(两条)?

与其他知识点的关系

  • 向前:kp-015/016 的抽象让纯逻辑可测;kp-026 的架构决定分层方式。
  • 向后:kp-028 的审查清单包含「新代码是否带行为测试」;kp-019 的性能预算进 CI 是本篇的进阶扩展。

延伸阅读

  • Testing Library 原则(核心必读):<https://testing-library.com/docs/guiding-principles>
  • Vitest 文档:<https://vitest.dev/>
  • Playwright:<https://playwright.dev/>
  • Common mistakes with React Testing Library:<https://kentcdodds.com/blog/common-mistakes-with-react-testing-library>