Snapshot TestingVitest前端測試元件測試Jest前端教學

Snapshot Testing 教學:Vitest 快照測試完全指南

元件輸出變了卻不知道?Vitest 快照測試教學含 toMatchSnapshot、Inline Snapshot。自動捕捉變更,守護你的 UI 一致性。

· 5 分鐘閱讀

元件的 render 邏輯複雜,斷言 toHaveTextContenttoBeVisible 只能檢查部分輸出,卻沒有簡單的方法確認「整個 DOM 看起來都一樣」?Snapshot Testing 把元件的完整 DOM 輸出「截圖」存起來,每次執行測試時自動比對——有任何變更馬上發現。


前言:Snapshot Testing 是什麼?

Snapshot Testing 的核心思想很直覺:

  1. 第一次執行測試時:把元件 render 出來的 DOM 輸出存成一個 .snap 檔案
  2. 之後每次執行測試時:重新 render 元件,與 .snap 檔案比對
  3. 如果不同:測試失敗,標記出哪些地方變了

這個概念類似 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 元件》