- 1. Snapshot Testing คืออะไร?
- 2. รูปแบบ Snapshot ที่ควรรู้
- 3. เตรียม Workshop
- 4. เขียนฟังก์ชันที่ต้องการทดสอบ
- 5. External Snapshot ด้วย toMatchSnapshot()
- 6. Inline Snapshot และ Error Snapshot
- 7. File Snapshot: HTML, SVG และข้อความ
- 8. จัดการ Dynamic Values
- 9. ทดลองให้ Snapshot Fail แล้ววิเคราะห์ Diff
- 10. Git และ CI/CD กับ GitHub Actions
- 11. ใช้ Snapshot กับ UI อย่างไร?
- 12. Snapshot Testing เทียบกับ Unit Testing แบบ Assertion
- 13. Best Practices และ Anti-patterns
- 14. แบบฝึกหัด Workshop
- 15. สรุป
- เอกสารอ้างอิง

ระดับ: เริ่มต้น–กลาง | กลุ่มเป้าหมาย: นักพัฒนา JavaScript/TypeScript และผู้ทดสอบซอฟต์แวร์
ผลลัพธ์การเรียนรู้: สร้าง snapshot, วิเคราะห์ diff, อัปเดต baseline อย่างถูกวิธี และประยุกต์ใช้ใน CI/CD ได้
#1. Snapshot Testing คืออะไร?
Snapshot Testing คือเทคนิคการทดสอบที่บันทึกผลลัพธ์จากโปรแกรมเป็นค่าอ้างอิง (baseline / snapshot) แล้วนำผลลัพธ์ในการรันทดสอบครั้งถัดไปมาเปรียบเทียบ หากแตกต่างกัน การทดสอบจะล้มเหลวเพื่อให้ผู้พัฒนาตรวจสอบว่าเป็นการเปลี่ยนแปลงที่ตั้งใจหรือเป็น regression
Snapshot สามารถใช้ตรวจสอบออบเจ็กต์ JavaScript, ข้อความ, HTML, SVG, รูปแบบ error และผลลัพธ์ที่ serialize ได้ ไม่ใช่การถ่ายภาพหน้าจอเสมอไป
flowchart LR
A[Run Test] --> B[Generate Actual Output]
B --> C{Existing Baseline?}
C -->|No: local run| D[Create Snapshot]
C -->|Yes| E{Compare Actual / Expected}
E -->|Same| F[PASS]
E -->|Different| G[FAIL + Diff]
G --> H{Change Intended?}
H -->|No| I[Fix Code]
H -->|Yes| J[Review & Update Baseline]
J --> E
แนวคิดสำคัญ: Snapshot บอกว่าผลลัพธ์ เปลี่ยนไป แต่ไม่ได้ยืนยันโดยลำพังว่า ผลลัพธ์ถูกต้องตามความต้องการทางธุรกิจ ดังนั้นควรใช้ควบคู่กับ assertion ที่ตรวจสอบพฤติกรรมเฉพาะจุด
#2. รูปแบบ Snapshot ที่ควรรู้
| รูปแบบ | Matcher | จัดเก็บค่าอ้างอิง | เหมาะสำหรับ |
|---|---|---|---|
| External Snapshot | toMatchSnapshot() |
ไฟล์ __snapshots__/*.snap |
Object/Array หรือผลลัพธ์หลายส่วน |
| Inline Snapshot | toMatchInlineSnapshot() |
ในไฟล์ทดสอบโดยตรง | ค่าสั้น อ่านง่าย |
| File Snapshot | toMatchFileSnapshot() |
ไฟล์ปลายทาง เช่น .html, .svg, .txt |
ข้อความยาวหรือรูปแบบไฟล์ที่ต้องการ syntax highlighting |
| Error Snapshot | toThrowErrorMatchingInlineSnapshot() |
ในไฟล์ทดสอบ | รูปแบบข้อความข้อผิดพลาด |
| Visual Screenshot | toMatchScreenshot() |
ไฟล์ภาพ baseline (ต้องตั้งค่า Browser Mode) | การเปลี่ยนแปลงด้านภาพของ UI |
หมายเหตุ: Snapshot ของข้อมูล ≠ Visual Regression Testing การเปรียบเทียบ HTML string ไม่สามารถตรวจพบทุกความแตกต่างในการเรนเดอร์จริง เช่น ฟอนต์ สี หรือการจัดวางองค์ประกอบ
#3. เตรียม Workshop
อ้างอิงเอกสาร Getting Started ของ Vitest รุ่นปัจจุบัน (ตรวจสอบวันที่ 17 ตุลาคม 2026): แนะนำ Node.js 22.12+ และ Vite 6.4+ สำหรับรุ่นปัจจุบัน หากใช้งานโปรเจ็กต์เดิมให้ตรวจสอบข้อกำหนดของเวอร์ชันที่ติดตั้งด้วย
mkdir vitest-snapshot-workshop
cd vitest-snapshot-workshop
npm init -y
npm install -D vitest
mkdir -p src tests/fixtures
แก้ package.json ให้มี type และ scripts (เก็บ dependencies ที่ npm สร้างไว้):
{
"type": "module",
"scripts": {
"test": "vitest",
"test:ci": "vitest run",
"test:update": "vitest run --update"
}
}
JSON ข้างต้นเป็นส่วนที่ต้องเพิ่ม/ปรับ ไม่ใช่ไฟล์
package.jsonทั้งหมด โปรดคงdevDependenciesที่สร้างจากnpm installไว้
โครงสร้างสุดท้ายหลังรันทดสอบครั้งแรก:
vitest-snapshot-workshop/
├── package.json
├── package-lock.json
├── src/
│ └── product.js
├── tests/
│ ├── product.test.js
│ ├── __snapshots__/
│ │ └── product.test.js.snap
│ └── fixtures/
│ └── product-card.html
└── .gitignore
#4. เขียนฟังก์ชันที่ต้องการทดสอบ
สร้างไฟล์ src/product.js:
export function summarizeProduct(product) {
return {
sku: product.sku,
name: product.name,
price: product.price,
availability: product.stock > 0 ? 'in-stock' : 'out-of-stock',
tags: [...product.tags].sort(),
}
}
export function renderProductCard(product) {
return `<article class="product-card">
<h2>${product.name}</h2>
<p class="price">${product.price} THB</p>
</article>`
}
export function createOrderReceipt(orderId) {
return {
orderId,
generatedAt: new Date(),
receiptNumber: Math.floor(Math.random() * 1_000_000),
status: 'issued',
}
}
export function validateQuantity(quantity) {
if (quantity <= 0) {
throw new Error('Quantity must be greater than zero')
}
return true
}
ข้อควรระวัง: ฟังก์ชัน renderProductCard ใช้ค่าตัวอย่างที่ควบคุมได้เท่านั้น หากนำไปใช้งานจริงต้อง escape ข้อมูลที่ไม่เชื่อถือก่อนแทรกลงใน HTML เพื่อป้องกัน XSS
#5. External Snapshot ด้วย toMatchSnapshot()
สร้างไฟล์ tests/product.test.js:
import { describe, expect, test } from 'vitest'
import {
summarizeProduct,
renderProductCard,
createOrderReceipt,
validateQuantity,
} from '../src/product.js'
const product = {
sku: 'P-001',
name: 'Mechanical Keyboard',
price: 1290,
stock: 8,
tags: ['accessory', 'keyboard'],
}
describe('Vitest Snapshot Workshop', () => {
test('product summary: external snapshot', () => {
expect(summarizeProduct(product)).toMatchSnapshot()
})
test('product title: inline snapshot', () => {
expect(product.name.toUpperCase()).toMatchInlineSnapshot()
})
test('HTML output: file snapshot', async () => {
const html = renderProductCard(product)
await expect(html).toMatchFileSnapshot('./tests/fixtures/product-card.html')
})
test('dynamic order fields: property matchers', () => {
const receipt = createOrderReceipt('ORD-1001')
expect(receipt).toMatchSnapshot({
generatedAt: expect.any(Date),
receiptNumber: expect.any(Number),
})
})
test('validation error: inline snapshot', () => {
expect(() => validateQuantity(0)).toThrowErrorMatchingInlineSnapshot()
})
test('critical business rule: direct assertion', () => {
expect(summarizeProduct({ ...product, stock: 0 }).availability)
.toBe('out-of-stock')
})
})
ทดสอบครั้งแรก:
npm run test:ci
Vitest จะสร้าง baseline สำหรับ External/Inline/File Snapshots ในการรัน local ที่อนุญาตให้สร้าง snapshot ใหม่ จากนั้นตรวจสอบไฟล์ที่ถูกสร้างหรือแก้ไข และรันอีกครั้งเพื่อตรวจผลการเปรียบเทียบ
npm run test:ci
ตัวอย่างลักษณะเนื้อหาในไฟล์ tests/__snapshots__/product.test.js.snap (รูปแบบจริงขึ้นอยู่กับเวอร์ชันและการจัดรูปแบบ):
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Vitest Snapshot Workshop > product summary: external snapshot 1`] = `
{
"availability": "in-stock",
"name": "Mechanical Keyboard",
"price": 1290,
"sku": "P-001",
"tags": [
"accessory",
"keyboard",
],
}
`;
นี่คือตัวอย่าง เพื่ออธิบายรูปแบบ ไฟล์ snapshot ที่ Vitest สร้างจริงเป็นแหล่งอ้างอิงของโครงการนั้น ๆ
#6. Inline Snapshot และ Error Snapshot
หลังรันทดสอบครั้งแรก Vitest จะเติมค่าที่ capture ได้ลงในไฟล์ทดสอบ เช่น:
test('product title: inline snapshot', () => {
expect(product.name.toUpperCase())
.toMatchInlineSnapshot('"MECHANICAL KEYBOARD"')
})
ข้อดีคืออ่านค่า expected ได้ในจุดเดียวกับโค้ดทดสอบ แต่ถ้าผลลัพธ์ยาวมาก ให้ย้ายไป External หรือ File Snapshot จะดูแลรักษาได้ง่ายกว่า
สำหรับ Error Snapshot ต้องระวังข้อความที่ผันแปรตาม runtime, stack trace, path หรือเวอร์ชัน dependencies ซึ่งอาจทำให้เกิดการเปลี่ยนแปลงโดยไม่จำเป็น
#7. File Snapshot: HTML, SVG และข้อความ
ตัวอย่างใน Workshop:
test('HTML output: file snapshot', async () => {
const html = renderProductCard(product)
await expect(html).toMatchFileSnapshot('./tests/fixtures/product-card.html')
})
หลักการสำคัญ:
toMatchFileSnapshot()เป็น asynchronous matcher ต้องใช้await- ให้ใช้ตำแหน่งไฟล์ baseline ที่กำหนดเองอย่างชัดเจน
- ไม่ควรเขียน File Snapshot ไปทับพาธที่ Vitest ดูแลสำหรับ External Snapshot เช่น
__snapshots__/*.snap - ตรวจสอบไฟล์
.htmlหรือ.svgด้วย diff และเครื่องมือเฉพาะรูปแบบไฟล์เพิ่มเติมได้
#8. จัดการ Dynamic Values
ข้อมูลอย่าง Date, UUID, random number และค่า timestamp ทำให้ snapshot เปลี่ยนทุกครั้งได้
วิธีที่ 1: Property Matchers ตรวจสอบชนิดข้อมูลเฉพาะ field ที่ผันแปร:
expect(receipt).toMatchSnapshot({
generatedAt: expect.any(Date),
receiptNumber: expect.any(Number),
})
วิธีที่ 2: Normalize ค่าก่อน Snapshot เหมาะกับข้อมูลที่ไม่ต้องสนใจค่าเฉพาะ:
const normalized = {
...receipt,
generatedAt: '<dynamic-date>',
receiptNumber: '<dynamic-number>',
}
expect(normalized).toMatchSnapshot()
วิธีที่ 3: ควบคุมเวลา / Random ด้วย fake timers หรือ dependency injection เมื่อการทดสอบจำเป็นต้องยืนยันค่าจริง (ต้องระมัดระวัง cleanup เพื่อลดผลกระทบข้าม test)
#9. ทดลองให้ Snapshot Fail แล้ววิเคราะห์ Diff
- แก้ไข
price: 1290เป็นprice: 1490ในข้อมูลproductภายในไฟล์ทดสอบ - รัน
npm run test:ci - สังเกตผล
FAILและ diff ที่เปรียบเทียบ1290กับ1490 - ตรวจว่าเป็นการเปลี่ยนแปลงตาม requirement หรือเป็น regression
- ถ้าตั้งใจเปลี่ยน ให้อัปเดต snapshot หลัง code review เท่านั้น
# อัปเดตเฉพาะเมื่อยืนยันผลลัพธ์ใหม่ว่าถูกต้องแล้ว
npm run test:update
# รันตรวจสอบอีกครั้งโดยไม่อัปเดตอัตโนมัติ
npm run test:ci
สำหรับ watch mode (npm test) สามารถกด u เพื่ออัปเดต snapshot ที่ล้มเหลวได้
อย่าอัปเดต snapshot เพื่อให้ test กลับมาเขียวโดยไม่ตรวจ diff เพราะอาจยอมรับ regression เป็น baseline ใหม่โดยไม่รู้ตัว
#10. Git และ CI/CD กับ GitHub Actions
ไฟล์ .gitignore ตัวอย่าง:
node_modules/
.vitest/
coverage/
ควร commit __snapshots__/*.snap, ไฟล์ baseline ภายใต้ tests/fixtures และไฟล์ที่มี Inline Snapshots เพื่อให้ทีมสามารถ review การเปลี่ยนแปลงได้
ตัวอย่าง .github/workflows/test.yml:
name: Vitest Snapshot Tests
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
- name: Install dependencies
run: npm ci
- name: Run tests (no snapshot updates)
run: npm run test:ci
env:
CI: 'true'
สำคัญ: ต้องมี package-lock.json ใน repository จึงใช้ npm ci ได้ และควรหลีกเลี่ยงการตั้งค่า CI ให้อัปเดต baseline โดยอัตโนมัติ
พฤติกรรมปกติของ Vitest เมื่อ CI=true คือ ไม่เขียน snapshot ใหม่ และให้การรันทดสอบล้มเหลวเมื่อ baseline หาย ไม่ตรง หรือกลายเป็น obsolete snapshot (ยกเว้นมีการตั้งค่า override)
#11. ใช้ Snapshot กับ UI อย่างไร?
#11.1 HTML Snapshot
ทดสอบผลลัพธ์ markup หรือ serialized component เพื่อจับความเปลี่ยนแปลงในโครงสร้างของข้อความ/DOM ที่ serialize แล้ว แต่จะไม่ได้ทดสอบ CSS layout จริง
#11.2 Visual Snapshot / Screenshot
ถ้าเป้าหมายคือการตรวจสี ระยะห่าง layout หรือภาพของหน้าเว็บ ให้ใช้ Vitest Browser Mode และ toMatchScreenshot() ซึ่งเป็น workflow แยกจากตัวอย่าง Node-only ในบทความนี้:
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'
test('button visual baseline', async () => {
// ต้องเตรียม Browser Mode และเรนเดอร์ UI ก่อน assertion นี้
await expect(page.getByRole('button')).toMatchScreenshot('primary-button')
})
การทดสอบลักษณะนี้ต้องควบคุม viewport, browser, fonts, animations และ environment เพื่อไม่ให้ผลลัพธ์ผันผวน ตัวอย่างข้างต้นเป็น แนวทางการใช้ assertion ไม่ใช่ตัวอย่างพร้อมรันในโปรเจ็กต์ Node-only
#11.3 Accessibility (ARIA) Snapshot
Vitest Browser Mode ยังรองรับการเปรียบเทียบ accessibility tree ด้วย toMatchAriaSnapshot() หรือ toMatchAriaInlineSnapshot() ช่วยตรวจสอบโครงสร้าง role/accessible name บางส่วน แต่ไม่ได้ทดแทนการตรวจ accessibility แบบครบถ้วน
#12. Snapshot Testing เทียบกับ Unit Testing แบบ Assertion
| ประเด็น | Snapshot Testing | Direct Assertion |
|---|---|---|
| ตรวจการเปลี่ยนแปลงหลาย field พร้อมกัน | เหมาะมาก | ต้องเขียนหลาย assertion |
| ระบุ business rule ชัดเจน | ไม่ชัดเท่า | ชัดเจนกว่า |
| ผลลัพธ์ที่เปลี่ยนบ่อย | เสี่ยง flaky / review noise | เลือกตรวจเฉพาะ field ได้ |
| Review ผลลัพธ์ที่ตั้งใจเปลี่ยน | ตรวจ diff baseline | ตรวจ expectation ในโค้ด |
| การบำรุงรักษา | อาจแพงหาก baseline ใหญ่ | เหมาะสำหรับ requirement เฉพาะ |
แนวทางที่เหมาะสม: ใช้ Snapshot เพื่อปกป้องภาพรวมของผลลัพธ์ที่ค่อนข้างเสถียร และใช้ Direct Assertion ตรวจสอบ invariant, edge cases และกฎธุรกิจสำคัญ
#13. Best Practices และ Anti-patterns
ควรทำ
- สร้าง snapshot จากผลลัพธ์ที่ deterministic หรือ normalize ค่าที่ผันแปร
- ตั้งชื่อ test ให้บอกพฤติกรรมและความคาดหวัง
- เก็บ baseline ใน version control และตรวจ
git diffทุกครั้งที่อัปเดต - ทำ snapshot ให้เล็กพอสำหรับ human review
- เพิ่ม direct assertions ในส่วนสำคัญของ business logic
- ใช้
vitest runใน CI และตรวจสอบว่ามี snapshot ครบถ้วน - ระวัง serializer, locale, timezone และ environment ที่ทำให้ output เปลี่ยน
ไม่ควรทำ
- snapshot ทั้งหน้าเว็บขนาดใหญ่อย่างไม่มีจุดประสงค์
- รวม fields ที่สร้างค่าใหม่ทุกครั้งไว้ใน baseline แบบ literal
- กด
uโดยไม่ดู diff - ใช้ Snapshot แทน end-to-end, accessibility หรือ visual testing ทุกกรณี
- อนุมานว่า Snapshot PASS แปลว่าตรงตาม requirement เสมอ
#14. แบบฝึกหัด Workshop
- เพิ่มสินค้า
P-002ที่stock: 0แล้วสร้าง Snapshot ของsummarizeProduct() - เพิ่ม field
discountPriceและสังเกต snapshot diff - เปลี่ยนค่า
generatedAtจาก property matcher เป็นค่า literal แล้วอธิบายว่าทำไม test จึงล้มเหลวบ่อย - เพิ่ม HTML
data-testidแล้วทดสอบ File Snapshot - จัดทำ Pull Request ที่มี snapshot diff พร้อมคำอธิบายว่าเปลี่ยน baseline เพราะอะไร
- เพิ่ม assertion ทดสอบว่า
discountPrice <= priceโดยไม่ใช้ Snapshot
#คำถามท้ายบท
- เหตุใด Snapshot Testing จึงไม่สามารถยืนยันความถูกต้องตาม requirement ได้เพียงลำพัง?
- External Snapshot กับ Inline Snapshot มีข้อดีต่างกันอย่างไร?
- จะจัดการกับวันที่และ Random ID เพื่อไม่ให้ snapshot flaky อย่างไร?
- ทำไม CI ไม่ควรอัปเดต Snapshot อัตโนมัติ?
- เมื่อไรจึงควรใช้ Visual Screenshot แทน HTML Snapshot?
#15. สรุป
Vitest Snapshot Testing เป็นเครื่องมือที่ช่วยตรวจจับ unexpected changes ของผลลัพธ์ซอฟต์แวร์ได้รวดเร็ว รูปแบบที่ใช้งานบ่อย ได้แก่ toMatchSnapshot(), toMatchInlineSnapshot() และ toMatchFileSnapshot() แต่คุณภาพของการทดสอบไม่ได้เกิดจากจำนวน snapshot เพียงอย่างเดียว—ต้องมี baseline ที่ผ่านการตรวจสอบ, ผลลัพธ์ที่เสถียร และ assertion ที่สื่อถึงข้อกำหนดของระบบด้วย
#เอกสารอ้างอิง
- Vitest, Snapshot Guide — https://vitest.dev/guide/snapshot.html
- Vitest, Snapshot Testing Tutorial — https://vitest.dev/guide/learn/snapshots.html
- Vitest, Getting Started — https://vitest.dev/guide/
- Vitest, CLI / Update Option — https://vitest.dev/guide/cli
- Vitest, Visual Regression Testing — https://vitest.dev/guide/browser/visual-regression-testing
- Vitest, ARIA Snapshots — https://vitest.dev/guide/browser/aria-snapshots