📋 目錄
元件的 render 邏輯複雜,斷言
toHaveTextContent或toBeVisible只能檢查部分輸出,卻沒有簡單的方法確認「整個 DOM 看起來都一樣」?Snapshot Testing 把元件的完整 DOM 輸出「截圖」存起來,每次執行測試時自動比對——有任何變更馬上發現。
前言:Snapshot Testing 是什麼?
Snapshot Testing 的核心思想很直覺:
- 第一次執行測試時:把元件 render 出來的 DOM 輸出存成一個
.snap檔案 - 之後每次執行測試時:重新 render 元件,與
.snap檔案比對 - 如果不同:測試失敗,標記出哪些地方變了
這個概念類似 Visual Regression Testing(視覺回歸測試),但針對的是 DOM 結構,而不是視覺截圖。適合確認「這段 HTML 字串有沒有被意外修改」。
Vitest 的 Snapshot 支援
Vitest 原生支援 Snapshot,不需要任何額外套件。
npm install -D vitest @vitest/ui jsdom
基本用法
// src/components/Alert.tsx
import React from 'react';
interface AlertProps {
type?: 'info' | 'success' | 'warning' | 'error';
title?: string;
children: React.ReactNode;
}
const styles = {
info: { backgroundColor: '#e0f2fe', borderColor: '#0ea5e9' },
success: { backgroundColor: '#dcfce7', borderColor: '#22c55e' },
warning: { backgroundColor: '#fef9c3', borderColor: '#f59e0b' },
error: { backgroundColor: '#fee2e2', borderColor: '#ef4444' },
};
export function Alert({ type = 'info', title, children }: AlertProps) {
return (
<div style={{ padding: '1rem', border: '1px solid', borderRadius: '0.5rem', ...styles[type] }}>
{title && <strong>{title}</strong>}
<div>{children}</div>
</div>
);
}
// src/components/Alert.test.tsx
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { Alert } from './Alert';
describe('Alert 元件', () => {
it('info 類型的 Snapshot', () => {
const { container } = render(<Alert type="info" title="提示">這是內容</Alert>);
// toMatchSnapshot() 會把 DOM 輸出與 .snap 檔案比對
expect(container.firstChild).toMatchSnapshot();
});
it('error 類型的 Snapshot', () => {
const { container } = render(
<Alert type="error" title="錯誤">
發生了一些問題
</Alert>
);
expect(container.firstChild).toMatchSnapshot();
});
});
第一次執行時,Vitest 會自動建立 .snap 檔案:
// src/components/__snapshots__/Alert.test.tsx.snap
exports[`Alert 元件 info 類型的 Snapshot 1`] = `
<div
style="[object Object]"
>
<strong>
提示
</strong>
<div>
這是內容
</div>
</div>
`;
Snapshot 的更新
當你預期要變更 Snapshot 時(例如修改了元件的 DOM 結構),需要更新:
# 更新所有 Snapshot
npx vitest run --update-snapshots
# 只更新符合條件的 Snapshot
npx vitest run --update-snapshots src/components/Alert.test.tsx
內聯 Snapshot
如果不想建立 .snap 檔案,可以用 toMatchInlineSnapshot() 把 Snapshot 存在測試檔案裡:
it('用內聯 Snapshot', () => {
const { container } = render(<Alert type="success">操作成功</Alert>);
// Snapshot 會內聯在測試檔案中
expect(container.firstChild).toMatchInlineSnapshot(`
<div
style="[object Object]"
>
<div>
操作成功
</div>
</div>
`);
});
內聯 Snapshot 的好處是 Snapshot 和測試邏輯在同一個檔案裡,適合 Snapshot 很小、很少的元件。
Snapshot 的常見應用場景
1. API 回應的結構驗證
Snapshot 不只能用於 React 元件,也可以用於任何可序列化的資料:
import { describe, it, expect } from 'vitest';
describe('API 回應結構', () => {
it('GET /api/users 回應格式應該符合預期', async () => {
const response = await fetch('/api/users');
const data = await response.json();
expect(data).toMatchSnapshot();
});
it('錯誤回應格式應該一致', () => {
const errorResponse = {
error: 'Validation Error',
details: [
{ field: 'email', message: '格式不正確' },
{ field: 'password', message: '長度不足' },
],
timestamp: '2026-03-29T12:00:00Z',
};
expect(errorResponse).toMatchSnapshot();
});
});
2. Markdown 渲染結果驗證
import { describe, it, expect } from 'vitest';
import { renderMarkdown } from '../utils/markdown';
describe('Markdown 渲染', () => {
it('應該正確渲染標題和列表', () => {
const md = '# Hello\\n\\n- Item 1\\n- Item 2';
const html = renderMarkdown(md);
expect(html).toMatchSnapshot();
});
});
Snapshot 的陷阱
Snapshot 測試有個常見的誤用:把 Snapshot 當成「正確性」的證明。
Snapshot 只告訴你「有沒有變化」,不告訴你「變化是不是正確的」。如果 Snapshot 建立時的輸出本來就是錯的,Snapshot 會永遠保護那個錯誤。
正確的使用方式:
- Snapshot 是防止意外變更的工具,不是驗證正確性的工具
- 建立 Snapshot 前,先確認元件輸出是正確的
- Snapshot 失敗時,要人為確認差異是「預期的」還是「意外的」
Snapshot 與 Component Testing 的組合
Snapshot 適合測試「不該變的東西不變了」,Component Testing 適合測試「互動邏輯是否正確」。兩者一起用,各司其職:
// Component Testing:測試互動邏輯
it('點擊應該切換展開/收起', async () => {
const { getByRole } = render(<Accordion title="點我">內容</Accordion>);
expect(getByRole('button')).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(getByRole('button'));
expect(getByRole('button')).toHaveAttribute('aria-expanded', 'true');
});
// Snapshot:確保結構沒有意外改變
it('預設狀態的 DOM 結構應該保持一致', () => {
const { container } = render(<Accordion title="標題">內容</Accordion>);
expect(container.firstChild).toMatchSnapshot();
});
常見問題
Q:Snapshot 測試和 Visual Regression Testing 有什麼不同?
A:Visual Regression Testing 產生圖片截圖,Snapshot Testing 產生DOM 字串。Snapshot 比對文字,不需要瀏覽器,執行更快;但它看不出「CSS 樣式」導致的視覺問題。
Q:Snapshot 檔案要放 Git 嗎?
A:要。.snap 檔案應該放進 Git,這樣團隊成員每次 Pull 的時候都會拿到最新的 Snapshot,比對也會自動觸發。
Q:Snapshot 失敗了怎麼知道差在哪裡?
A:Vitest 會自動產生 *.actual.* 和 *.expected.* 的 diff 檔案(文字格式)。打開比對即可看到具體差異,確認是預期變更還是意外破壞。
Q:Snapshot 要測試所有元件嗎?
A:不需要。Snapshot 適合用在:穩定的展示型元件(很少變更,但變更時後果嚴重)、API 回應結構、複雜的資料轉換結果。互動邏輯之類的用 Component Testing 來測就好。
總結:Snapshot 是防止意外破壞的第一道防線
Snapshot Testing 的核心價值:在不用寫詳細斷言的情況下,確保「輸出維持不變」。
這篇文章涵蓋:
- Snapshot Testing 的核心理念
- Vitest 的 Snapshot 基本用法(
toMatchSnapshot) - 內聯 Snapshot(
toMatchInlineSnapshot) - 應用場景:API 回應結構、Markdown 渲染
- Snapshot 的使用原則與常見陷阱
- Snapshot 與 Component Testing 的組合
下一步:在你的專案選一個穩定的展示型元件,加上 Snapshot 測試。從 Simple Badge 或 Alert 這類簡單元件開始,建立第一道防止意外破壞的防線。
想了解如何用 Vitest 測試 React 元件,可參考 《Component Testing:用 Vitest 測試你的 React/Vue 元件》。