React 测试策略与组件测试
React 测试策略与组件测试:从单元到 E2E 的完整体系
引言
「没有测试的重构是赌博」——这句话在 React 应用中尤其贴切。组件化让 UI 具备可测性,但测试策略的混乱(什么都测、什么都覆盖不全)同样常见。本文将从测试金字塔讲起,系统梳理 React 应用的测试分层:单元测试、组件测试、集成测试与端到端测试,深入讲解 Testing Library 的使用哲学、异步测试、mock 策略与测试覆盖率管理。
一、测试金字塔与分层策略
React 测试的金字塔遵循「底层多、上层少」的原则:单元测试量大便宜,E2E 测试量小昂贵。每一层解决不同层次的问题。
测试金字塔
flowchart TB
classDef p1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef p2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
classDef p3 fill:#e0f7fa,stroke:#00838f,color:#004d40
classDef p4 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
A[E2E 端到端测试<br/>数量少 成本高] --> B[模拟真实用户流程]
B --> C[Playwright / Cypress]
D[集成测试<br/>中等数量] --> E[多组件协作]
E --> F[Testing Library 组合]
G[组件测试<br/>数量多] --> H[单组件行为]
H --> I[render + 断言]
J[单元测试<br/>数量最多 最快] --> K[纯函数/工具/Hook]
K --> L[Vitest / Jest]
click B "https://playwright.dev/" "Playwright 文档"
click G "https://testing-library.com/react" "Testing Library 文档"
click L "https://vitest.dev/" "Vitest 文档"
class A p1
class B p2
class C p3
class D p1
class E p2
class F p3
class G p1
class H p2
class I p3
class J p1
class K p2
class L p3
每层测试关注的问题
| 层级 | 工具 | 关注点 | 运行时间 |
|---|---|---|---|
| 单元测试 | Vitest/Jest | 纯函数、工具、Hook | 毫秒级 |
| 组件测试 | Testing Library | 渲染、交互、状态 | 秒级 |
| 集成测试 | 组件组合 | 多组件协作流 | 秒级 |
| E2E 测试 | Playwright | 用户完整旅程 | 分钟级 |
二、Testing Library 的测试哲学
Testing Library 的核心哲学是:像用户一样测试。用户不关心实现细节(class 名、内部状态),只关心「看到什么、怎么交互」。
查询方式优先级
flowchart TD
classDef q1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef q2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
classDef q3 fill:#ffebee,stroke:#c62828,color:#b71c1c
A[查询元素] --> B[角色查询<br/>getByRole 优先]
A --> C[文本查询<br/>getByText]
A --> D[标签查询<br/>getByLabelText]
A --> E[占位符查询]
A --> F[测试 id 查询<br/>最后手段]
B --> B1[最贴近用户感知]
B --> B2[无障碍友好]
C --> C1[可见文本内容]
D --> D1[表单字段关联]
F --> F1[结构耦合<br/>尽量避免]
click B "https://testing-library.com/docs/queries/byrole" "byRole 查询文档"
class A q1
class B q1
class C q2
class D q2
class E q2
class F q3
查询优先级的实践示例
javascript123456789// 推荐:角色查询 screen.getByRole("button", { name: "保存" }); screen.getByRole("heading", { name: "文章标题" }); // 表单:标签关联 screen.getByLabelText("邮箱"); // 避免:测试 id(实现细节) screen.getByTestId("save-btn");
黄金法则:查询方式越接近「用户如何找到这个元素」,测试就越健壮——用户通过按钮文字找到按钮,而不是通过 data-testid。
三、异步测试与等待策略
React 测试中最大的挑战是异步:数据请求、延迟渲染、过渡动画。Testing Library 提供 waitFor 与 findBy* 查询处理。
异步测试模式
flowchart TB
classDef a1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef a2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
classDef a3 fill:#e0f7fa,stroke:#00838f,color:#004d40
A[渲染组件] --> B[初始状态断言]
B --> C{异步行为}
C -->|数据加载| D[findByText 自动等待]
C -->|加载态| E[断言 spinner 出现]
C -->|加载完成| F[断言内容出现]
C -->|加载失败| G[findByRole alert]
F --> H[断言数据正确]
click D "https://testing-library.com/docs/dom-testing-library/api-async" "异步 API 文档"
class A a1
class B a1
class C a1
class D a3
class E a2
class F a3
class G a2
class H a3
三种异步查询对比
| 查询 | 行为 | 适用场景 |
|---|---|---|
getBy* |
立即查找,找不到抛错 | 同步渲染断言 |
queryBy* |
立即查找,找不到返回 null | 断言「不存在」 |
findBy* |
轮询等待直到出现 | 异步渲染断言 |
注意:findBy* 默认超时 1 秒,可传 { timeout } 调整;测试中避免使用裸 setTimeout 等待,用 waitFor 包裹更稳定。
四、Mock 策略:隔离外部依赖
组件测试需要隔离外部依赖:网络请求、路由、时钟、浏览器 API。Mock 策略的核心是「测试行为,不测实现」。
Mock 分类
flowchart LR
classDef m1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef m2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
classDef m3 fill:#e0f7fa,stroke:#00838f,color:#004d40
A[Mock 分类] --> B[网络请求<br/>MSW / vi.mock]
A --> C[时钟<br/>vi.useFakeTimers]
A --> D[浏览器 API<br/>jsdom / mock]
A --> E[第三方模块<br/>vi.mock 替换]
B --> B1[模拟成功/失败/慢响应]
C --> C1[前进时间 触发定时器]
D --> D1[matchMedia / ResizeObserver]
E --> E1[替换为桩实现]
click B "https://mswjs.io/" "MSW 文档"
class A m1
class B m1
class C m2
class D m2
class E m2
class B1 m3
class C1 m3
class D1 m3
class E1 m3
网络请求 Mock 的推荐方案
MSW(Mock Service Worker) 拦截真实网络层,与浏览器/Node 环境兼容,测试代码无需侵入组件:
sequenceDiagram
participant T as 测试
participant M as MSW
participant C as 组件
T->>M: 注册 mock handler
T->>C: 渲染组件
C->>M: 发起真实 fetch
M->>M: 拦截 返回 mock 数据
M-->>C: 响应
C->>C: 正常渲染数据
五、Hook 测试:renderHook 与 act
Hook 是 React 逻辑复用的载体,renderHook 允许不渲染 UI 直接测试 Hook 行为。
Hook 测试模式
flowchart TD
classDef h1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef h2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
classDef h3 fill:#e0f7fa,stroke:#00838f,color:#004d40
A[renderHook 挂载 Hook] --> B[断言初始值]
B --> C{触发更新}
C -->|act 包裹| D[调用返回的 setter]
C -->|rerender| E[传入新 props]
D --> F[断言新状态]
E --> F
F --> G{副作用测试}
G --> H[waitFor 等待 Effect 完成]
click A "https://react.dev/reference/react-dom/test-utils/renderHook" "renderHook 文档"
class A h1
class B h1
class C h2
class D h3
class E h3
class F h3
class G h2
class H h3
示例:测试 useCounter
javascript12345678910test("计数器可以增减", () => { const { result, rerender } = renderHook(() => useCounter(0)); expect(result.current.count).toBe(0); act(() => result.current.increment()); expect(result.current.count).toBe(1); rerender(); expect(result.current.count).toBe(1); // 引用稳定 });
六、E2E 测试:真实用户旅程
E2E 测试用真实浏览器模拟用户操作:登录、浏览、提交表单、验证结果。Playwright 是当前主流选择。
E2E 用例设计
flowchart TB
classDef e1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef e2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
classDef e3 fill:#e0f7fa,stroke:#00838f,color:#004d40
A[用户旅程拆分] --> B[核心路径<br/>登录-浏览-退出]
A --> C[关键路径<br/>创建文章-发布]
A --> D[边缘路径<br/>错误输入-校验]
B --> E[每个路径一个用例]
C --> E
D --> E
E --> F[CI 中稳定执行]
click B "https://playwright.dev/docs/test-intro" "Playwright 文档"
class A e1
class B e1
class C e2
class D e2
class E e3
class F e3
E2E 编写要点
- 用角色/文本定位器(
getByRole),不用 CSS 选择器; - 等待真实状态(
expect(...).toBeVisible()自动等待); - 控制外部依赖(mock 第三方 API);
- 独立数据(每个用例清理/重建测试数据);
- CI 中并行分片执行,控制总时长。
七、覆盖率与测试预算
覆盖率(Coverage)是参考而非目标:100% 覆盖率的代码依然可能没有测试到关键行为。合理的做法是设定「分层的覆盖目标」。
覆盖率管理
flowchart TD
classDef g1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef g2 fill:#fff3e0,stroke:#f57c00,color:#e65100
classDef g3 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
A[覆盖率策略] --> B[逻辑层 80%+<br/>纯函数/工具/Hook]
A --> C[组件层 70%+<br/>核心交互覆盖]
A --> D[E2E 核心路径<br/>全绿]
A --> E[阈值进 CI<br/>低于则失败]
click B "https://vitest.dev/guide/coverage" "Vitest 覆盖率"
class A g1
class B g2
class C g2
class D g2
class E g3
建议:把覆盖率阈值写入 CI(如 lines ≥ 75%),并配合「变更必须带测试」的 Code Review 规范,形成可持续的测试文化。
八、总结
React 测试体系的核心不是「工具越多越好」,而是「分层清晰、各司其职」:
- 单元测试:逻辑层快速验证,成本最低;
- 组件测试:Testing Library 像用户一样测,query 优先级是灵魂;
- 异步测试:findBy* 与 waitFor 是标准答案;
- Mock 策略:MSW 拦截网络,隔离外部依赖;
- E2E 测试:真实旅程验证,核心路径全绿;
- 覆盖率:分层阈值 + CI 门槛,形成闭环。
最后一条经验:测试的质量比数量重要——一个「像用户一样」的高价值测试,胜过十个断言实现细节的脆弱测试。下一篇我们将探讨 React 与 TypeScript 类型安全的完整实践。
