Skip to content
返回质量保障

快照测试

捕获组件渲染输出或序列化结构,任何非预期变更即失败。

为什么

对于多 prop 多状态的复杂 UI 组件,为每个渲染节点写显式断言不现实——有数百元素,期望 HTML 每次设计更新都变。快照测试一次捕获完整渲染输出,存为提交产物,每次后续运行把新输出与存快照比较。任何 diff 即测试失败,迫使开发者要么接受变更(更新快照)要么修回归。它是防范非预期 UI 变更最有效的方式,但它是钝器——抓住一切,包括无关变更。

何时使用

用于结构稳定的展示组件(卡片、表格行、表单布局)。避免用于有动态内容的组件(时间戳、随机 ID、异步数据)除非你 mock 它们。不要替代行为测试。

如何做

1. 选快照机制
   - Vitest/Jest: toMatchInlineSnapshot() 或 toMatchSnapshot()
   - React: react-test-renderer 或 @testing-library/react renderToStaticMarkup
   - Storybook: Storyshots 插件为每个 story 做视觉快照

2. 写第一个快照——用代表性 prop 渲染组件
   - 用真实但确定的数据(无 Date.now()、无 Math.random())
   - mock 任何异步数据获取使快照跨运行稳定
   - 第一次运行创建快照文件;提交到仓库

3. 后续运行测试把新输出与存快照比较
   - 匹配 → 测试通过(无非预期变更)
   - diff → 测试失败带可读 diff;开发者审查

4. 变更是有意的时更新快照
   - vitest -u(或 jest -u)重新生成快照
   - PR 中仔细审查 diff——盲目更新接受回归
   - 把更新快照连同代码变更一起提交

5. 周期性审查和清理快照
   - 快照随组件演进变陈旧;2000 行快照是坏味道
   - 若快照大多是动态噪声,换成针对性断言

示例

// src/components/ProductCard.test.tsx
import { render } from "@testing-library/react";
import { ProductCard } from "./ProductCard";

it("匹配已存快照", () => {
  const { container } = render(
    <ProductCard
      product={{
        id: 42,
        name: "Wireless Headphones",
        price: 79.99,
        image: "/img/42.png",
        inStock: true,
      }}
    />,
  );
  expect(container.firstChild).toMatchSnapshot();
});

// 第一次运行 → 创建 __snapshots__/ProductCard.test.tsx.snap:
// exports[`匹配已存快照 1`] = `<div class="card">...</div>`;

// 之后若有人改卡片布局,测试失败:
//   - 4 行删除
//   + 6 行新增
// 审查 diff。若有意: npx vitest -u 更新。

常见问题

  • ⚠盲目跑 vitest -u 让测试变绿而不审查 diff 接受回归——快照变成组件当前渲染的记录,啥也不守卫。
  • ⚠含未 mock 动态数据(时间戳、UUID、随机顺序)的组件快照每次都挂,开发者习惯性更新,训练团队完全无视快照失败。
  • ⚠巨型快照(500+ 行)任何小变更都挂并产生不可读 diff,没人审查——快照存在但无有用信号。
  • ⚠把快照测试当组件唯一测试意味着行为变更(按钮 onClick 不再触发)不被抓,因为快照只比渲染 HTML 非行为。

最佳实践

  • ✓用最小 prop 渲染组件保持快照小,复杂行为单独断言——30 行快照可审查;300 行不可。
  • ✓渲染前 mock 所有非确定源(Date.now、Math.random、UUID 生成、fetch)使快照跨运行字节相同,diff 总意味真变更。
  • ✓PR 中接受更新前始终审查快照 diff——若你无法解释 diff 每行,你对变更理解不够合并它。
  • ✓小组件用内联快照(toMatchInlineSnapshot)使期望输出在测试文件中,代码审查时无需开单独 .snap 文件。