# การทดสอบ Vue 3 ในปี 2026: Vitest, Vue Test Utils และคำถามสัมภาษณ์ > คู่มือปฏิบัติสำหรับการทดสอบ Vue ในปี 2026: การตั้งค่า Vitest, การ mount คอมโพเนนต์ด้วย Vue Test Utils, การทดสอบ composable และ Pinia store, การ mock API, การวัด coverage และคำถามสัมภาษณ์ที่ทีมผู้ว่าจ้างถามจริง - Published: 2026-06-21 - Updated: 2026-07-06 - Author: SharpSkill - Tags: vue, testing, vitest, vue-test-utils, interview - Reading time: 10 min --- การทดสอบ Vue คือเส้นแบ่งระหว่างการ refactor ด้วยความมั่นใจกับการเดาสุ่มที่เปราะบาง และในปี 2026 ชุดเครื่องมือได้รวมตัวกันรอบสองไลบรารีหลัก ได้แก่ [Vitest](https://vitest.dev/guide/) สำหรับเป็น test runner และ [Vue Test Utils](https://test-utils.vuejs.org/) สำหรับการ mount คอมโพเนนต์ คู่มือนี้จะพาไล่ตั้งแต่การตั้งค่าชุดเครื่องมือ การทดสอบคอมโพเนนต์และ composable การ mock dependency ไปจนถึงการตอบคำถามสัมภาษณ์เรื่องการทดสอบ Vue ที่ทีมผู้ว่าจ้างถามกันจริง > **ชุดเครื่องมือทดสอบ Vue ยุคใหม่ในปี 2026** > > คำแนะนำมาตรฐานจาก[เอกสาร Vue](https://vuejs.org/guide/scaling-up/testing.html) คือใช้ Vitest สำหรับ unit test และ component test ใช้ Vue Test Utils เป็น API ระดับล่างสำหรับการ mount และใช้ Playwright หรือ Cypress สำหรับการทดสอบแบบ end-to-end จุดเด่นคือ Vitest ใช้ config เดียวกันกับ Vite ที่แอปใช้อยู่ ดังนั้นเทสต์จึงวิ่งผ่าน transform pipeline ชุดเดียวกันเป๊ะ พร้อม hot reload ที่แทบจะทันที ## การตั้งค่า Vitest สำหรับโปรเจกต์ Vue 3 Vitest ใช้ pipeline ร่วมกับ Vite หมายความว่าไฟล์ config เพียงไฟล์เดียวขับเคลื่อนทั้ง dev server และ test runner ตัวเลือก `environment` ต้องกำหนดเป็น `jsdom` หรือ `happy-dom` เพื่อให้ component test มี DOM ให้ render ลงไป ส่วนแฟลก `globals: true` จะเปิดให้ `describe`, `it` และ `expect` ใช้งานได้โดยไม่ต้อง import ในทุกไฟล์ ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' import { fileURLToPath } from 'node:url' export default defineConfig({ plugins: [vue()], test: { // jsdom provides document/window so components can mount environment: 'jsdom', // expose describe/it/expect globally globals: true, // run this before each test file (matchers, cleanup) setupFiles: ['./tests/setup.ts'], // only pick up *.spec.ts and *.test.ts files include: ['**/*.{test,spec}.ts'], }, resolve: { alias: { // mirror the @ alias used across the app '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, }) ``` สำหรับ Vitest 3 config ด้านบนใช้งานได้ทันทีหลังติดตั้ง `vitest`, `@vue/test-utils`, `@vitejs/plugin-vue` และ `jsdom` และเนื่องจาก alias สะท้อน config ของตัวแอป การ import แบบ `@/composables/useCart` จึงถูก resolve เหมือนกันทั้งในเทสต์และในโปรดักชัน ## การ mount คอมโพเนนต์ด้วย Vue Test Utils Vue Test Utils เปิดจุดเข้าใช้งานสองทาง ได้แก่ `mount` ที่ render ต้นไม้คอมโพเนนต์ทั้งหมด และ `shallowMount` ที่แทนคอมโพเนนต์ลูกด้วย stub สำหรับ unit test ส่วนใหญ่ควรเลือก `mount` เพราะมันทดสอบพฤติกรรมการ render ของจริง ค่า `wrapper` ที่คืนกลับมาจะมีตัวช่วยค้นหาอย่าง `find`, `get` และ `text` ให้ใช้ยืนยันผลลัพธ์ที่ render ออกมา ```typescript // PriceTag.spec.ts import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import PriceTag from '@/components/PriceTag.vue' describe('PriceTag', () => { it('formats the amount as currency', () => { const wrapper = mount(PriceTag, { // props are passed through the mounting options props: { amount: 4200, currency: 'EUR' }, }) // get() throws if the selector is missing, unlike find() expect(wrapper.get('[data-test="price"]').text()).toBe('€42.00') }) it('applies a discount class when on sale', () => { const wrapper = mount(PriceTag, { props: { amount: 4200, currency: 'EUR', onSale: true }, }) // classes() returns the array of applied CSS classes expect(wrapper.get('[data-test="price"]').classes()).toContain('price--sale') }) }) ``` การเจาะไปที่ element ผ่านแอตทริบิวต์ `data-test` แทนที่จะใช้ CSS class ช่วยให้เทสต์ทนทานกว่า เพราะการปรับสไตล์ของ `.price--sale` จะไม่ทำให้ selector พัง มีเพียงการเปลี่ยนพฤติกรรมจริงเท่านั้นที่ทำให้เทสต์ล้ม การแยกส่วนแบบนี้เป็นแนวคิดที่พบซ้ำ ๆ ในการเขียนเทสต์ Vue ที่ดูแลรักษาง่าย ## การทดสอบ user event และ event ที่ถูก emit คอมโพเนนต์ที่มีการโต้ตอบจำเป็นต้องมีการยืนยันผลว่าจะเกิดอะไรขึ้นหลังการคลิกหรือการพิมพ์ Vue Test Utils คืนค่า promise จาก `trigger` และการ await มันจะ flush คิว reactivity ของ Vue เพื่อให้ DOM สะท้อนการอัปเดต ส่วนตัวช่วย `emitted` จะบันทึกทุก custom event ที่คอมโพเนนต์ยิงออกมา ซึ่งเป็นวิธีตรวจสอบสัญญาระหว่าง parent กับ child ```typescript // SearchBar.spec.ts import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import SearchBar from '@/components/SearchBar.vue' describe('SearchBar', () => { it('emits search with the typed query', async () => { const wrapper = mount(SearchBar) // setValue writes to the input and fires an input event await wrapper.get('input').setValue('vitest') // awaiting trigger flushes the reactivity queue before assertions await wrapper.get('[data-test="submit"]').trigger('click') // emitted() returns a record of every event and its payloads const events = wrapper.emitted('search') expect(events).toHaveLength(1) // events[0] is the first emission, [0] its first argument expect(events![0][0]).toBe('vitest') }) }) ``` > **อย่าลืม await การอัปเดต DOM เสมอ** > > Vue รวมการอัปเดต reactive เข้าด้วยกันแบบ asynchronous การลืม `await` การเรียก `trigger` หรือ `setValue` คือสาเหตุที่พบบ่อยที่สุดของเทสต์ Vue ที่ผลลัพธ์ไม่แน่นอน เพราะการยืนยันผลจะทำงานก่อนที่ DOM จะ re-render จึงอ่านค่าที่ยังไม่อัปเดต หากมีการเปลี่ยน state เกิดขึ้นนอกตัวช่วย event ให้ import `nextTick` จาก `vue` แล้ว `await nextTick()` ก่อนยืนยันผล ## การทดสอบ Vue composable แบบแยกอิสระ Composable ที่ใช้เพียง reactivity API (`ref`, `computed`, `watch`) ไม่จำเป็นต้องมีคอมโพเนนต์เลย เพราะมันคือฟังก์ชันธรรมดาที่คืน reactive state ออกมา ดังนั้นการเรียกใช้โดยตรงแล้วยืนยันผลบน `.value` ก็เพียงพอ นี่คือวิธีที่เร็วที่สุดและเจาะจงที่สุดในการครอบคลุมตรรกะทางธุรกิจ และเข้ากันได้ดีกับแพตเทิร์นใน[composable ขั้นสูงของ Vue 3](/blog/vue-nuxt/advanced-vue-3-composables-reusable-patterns) ```typescript // useCart.spec.ts import { describe, it, expect } from 'vitest' import { useCart } from '@/composables/useCart' describe('useCart', () => { it('tracks total price as items change', () => { // composables using only ref/computed run without mounting const { items, total, addItem } = useCart() expect(total.value).toBe(0) addItem({ id: 1, price: 1500, qty: 2 }) // computed total recalculates synchronously on state change expect(total.value).toBe(3000) addItem({ id: 2, price: 500, qty: 1 }) expect(items.value).toHaveLength(2) expect(total.value).toBe(3500) }) }) ``` Composable ที่พึ่งพา lifecycle hook อย่าง `onMounted` เป็นข้อยกเว้น เพราะมันต้องทำงานภายใน instance ของคอมโพเนนต์ วิธีแก้ที่นิยมใช้คือสร้างคอมโพเนนต์ตัวช่วยเล็ก ๆ ที่เรียก composable ใน `setup` แล้วเปิดผลลัพธ์ออกมา จากนั้นจึง mount คอมโพเนนต์ตัวช่วยนั้นด้วย Vue Test Utils ## การ mock การเรียก API ด้วย vi.mock คำขอผ่านเครือข่ายจริงทำให้เทสต์ช้าและไม่คงเส้นคงวา Vitest จึงแทนที่โมดูลด้วย `vi.mock` และสร้างฟังก์ชัน spy ด้วย `vi.fn` แพตเทิร์นด้านล่างจะ mock โมดูลที่ห่อ `fetch` ไว้ เพื่อให้ composable ที่กำลังทดสอบได้รับข้อมูลที่ควบคุมได้โดยไม่ต้องยิงไปที่เซิร์ฟเวอร์เลย ```typescript // useProducts.spec.ts import { describe, it, expect, vi, beforeEach } from 'vitest' import { useProducts } from '@/composables/useProducts' import { getProducts } from '@/api/products' // replace the whole module with mocked exports vi.mock('@/api/products', () => ({ getProducts: vi.fn(), })) describe('useProducts', () => { beforeEach(() => { // reset call history between tests to avoid leakage vi.mocked(getProducts).mockReset() }) it('exposes fetched products and clears loading', async () => { // define what the mocked API returns for this test vi.mocked(getProducts).mockResolvedValue([ { id: 1, name: 'Keyboard' }, ]) const { products, loading, load } = useProducts() await load() expect(getProducts).toHaveBeenCalledOnce() expect(loading.value).toBe(false) expect(products.value[0].name).toBe('Keyboard') }) }) ``` การ mock ที่ขอบเขตของโมดูลช่วยให้ตรรกะภายในของ composable ไม่ถูกแตะต้อง ในขณะที่ให้แต่ละเทสต์ควบคุม response ของ API ได้เต็มที่ รวมถึงเส้นทางที่เกิด error ด้วย การเรียก `mockRejectedValue` แทนจะทำให้เทสต์เพียงตัวเดียวสามารถตรวจสอบได้ว่าเมื่อเกิดความล้มเหลว state ของ error จะถูกตั้งค่าและ spinner จะหยุดหมุน ## การทดสอบ Pinia store ในปี 2026 Store เก็บ state ที่ใช้ร่วมกันข้ามคอมโพเนนต์ และแพ็กเกจทางการ `@pinia/testing` ทำให้ทดสอบได้ตรงไปตรงมา โดยค่าเริ่มต้น `createTestingPinia` จะ stub ทุก action ไว้ ดังนั้น component test จึงสามารถยืนยันได้ว่ามีการ dispatch action ออกไปโดยไม่ต้องรัน side effect ของมัน [คู่มือการทดสอบ Pinia](https://pinia.vuejs.org/cookbook/testing.html) แนะนำแนวทางนี้สำหรับเทสต์ระดับคอมโพเนนต์ ```typescript // CheckoutButton.spec.ts import { describe, it, expect, vi } from 'vitest' import { mount } from '@vue/test-utils' import { createTestingPinia } from '@pinia/testing' import CheckoutButton from '@/components/CheckoutButton.vue' import { useCartStore } from '@/stores/cart' describe('CheckoutButton', () => { it('dispatches checkout on click', async () => { const wrapper = mount(CheckoutButton, { global: { plugins: [ // vi.fn spies on actions instead of executing them createTestingPinia({ createSpy: vi.fn }), ], }, }) const store = useCartStore() await wrapper.get('[data-test="checkout"]').trigger('click') // the action is stubbed, so only the dispatch is asserted expect(store.checkout).toHaveBeenCalledOnce() }) }) ``` หากต้องการทดสอบตรรกะของ store เองแทนการทดสอบการผสานเข้ากับคอมโพเนนต์ ให้ส่ง `stubActions: false` เพื่อให้ action ทำงานตามปกติ แล้วจึงยืนยันผลบน state ที่ได้ การแยกระหว่างเทสต์คอมโพเนนต์ที่ stub action กับเทสต์ store ที่ใช้ action จริง ช่วยให้แต่ละ suite โฟกัสอยู่กับความรับผิดชอบเพียงอย่างเดียว ## การรันเทสต์ใน watch mode และการวัด coverage Vitest มาพร้อม watch mode ที่รันซ้ำเฉพาะเทสต์ที่ได้รับผลกระทบจากไฟล์ที่เพิ่งบันทึก ซึ่งย่นรอบ feedback ระหว่างการพัฒนาให้เหลือเพียงเสี้ยววินาที ส่วนการรายงาน coverage มีมาให้ในตัวผ่าน provider `@vitest/coverage-v8` และผสานเข้ากับ pipeline ของ continuous integration ได้อย่างเรียบร้อย โดยเทสต์ที่ล้มเหลวหรือ coverage ที่ถดถอยลงสามารถบล็อกการ merge ได้ ```typescript // vitest.config.ts (coverage section) export default defineConfig({ test: { coverage: { // v8 is the fastest provider and needs no instrumentation step provider: 'v8', reporter: ['text', 'html', 'lcov'], // fail CI when a threshold drops below the target thresholds: { statements: 80, branches: 75, functions: 80, }, // exclude generated and config files from the report exclude: ['**/*.config.ts', '**/types/**'], }, }, }) ``` การรัน `vitest` เพียงอย่างเดียวจะเริ่ม watch mode บนเครื่องท้องถิ่น ในขณะที่ `vitest run --coverage` จะรันแบบผ่านครั้งเดียวซึ่งเหมาะกับ CI การกำหนด threshold อย่างชัดเจนเปลี่ยน coverage จากตัวเลขไว้อวดให้กลายเป็นด่านคุณภาพที่บังคับใช้จริง เพราะ pull request ที่ทำให้ branch coverage ตกลงต่ำกว่า 75 เปอร์เซ็นต์จะล้มเหลวอัตโนมัติ ซึ่งกระตุ้นให้ผู้พัฒนาทดสอบเส้นทางที่ตัวเองเพิ่มเข้ามา ไม่ใช่เพียงเส้นทางที่ทุกอย่างราบรื่นเท่านั้น ## คำถามสัมภาษณ์เรื่องการทดสอบ Vue ที่พบบ่อยพร้อมคำตอบ คำถามเรื่องการทดสอบปรากฏในการสัมภาษณ์ Vue ระดับ senior เกือบทุกครั้ง เพราะมันเผยให้เห็นว่าผู้สมัครคิดเรื่องการดูแลรักษาโค้ดอย่างไร คำตอบด้านล่างครอบคลุมแนวคิดที่ถูกถามบ่อยที่สุด และชุดคำถามฉบับเต็มมีให้ฝึกใน[โมดูลคำถามสัมภาษณ์การทดสอบ Vue](/technologies/vue-nuxt/interview-questions/vue-testing) **ควรใช้ `shallowMount` แทน `mount` เมื่อใด?** เลือกใช้ `shallowMount` เมื่อคอมโพเนนต์ render ลูกที่หนักหรือถูกทดสอบไว้แล้ว และเทสต์สนใจเพียงตรรกะของ parent เท่านั้น การ stub ลูกจะแยก unit ให้อิสระและเร่งการ render ให้เร็วขึ้น แลกกับการที่ไม่ได้ตรวจสอบการผสานจริงระหว่างคอมโพเนนต์ **Vue Test Utils จัดการการอัปเดตแบบ asynchronous อย่างไร?** Vue นำการเปลี่ยนแปลง reactive ไปใช้ใน tick ถัดไป ตัวช่วย event อย่าง `trigger` และ `setValue` คืน promise ที่ resolve หลังจาก DOM อัปเดตแล้ว ดังนั้นการ await มันก็เพียงพอ ส่วน state ที่เปลี่ยนนอกตัวช่วยเหล่านั้น ให้ใช้ `await nextTick()` เพื่อบังคับให้คิว flush ก่อนที่การยืนยันผลจะทำงาน **`find` กับ `get` ต่างกันอย่างไร?** `find` คืน wrapper เปล่าเมื่อไม่มี element ตรงกันและจะไม่ throw ซึ่งเหมาะกับการยืนยันผลแบบ `expect(wrapper.find('.error').exists()).toBe(false)` ส่วน `get` จะ throw ทันทีเมื่อไม่พบ element ทำให้เป็นตัวเลือกที่ดีกว่าเมื่อคาดว่า element นั้นต้องมีอยู่ และการหายไปของมันคือความล้มเหลวที่แท้จริง **ทำไมจึงควรใช้แอตทริบิวต์ `data-test` แทน class selector?** ชื่อ class เปลี่ยนไปตามสไตล์และโครงสร้าง ดังนั้นเทสต์ที่ query จากมันจึงพังเมื่อมีการแก้ไขเชิงความสวยงาม การใช้ hook `data-test` โดยเฉพาะจะแสดงเจตนาของการทดสอบอย่างชัดเจนและอยู่รอดผ่านการ refactor ช่วยลด false negative **จะทดสอบ composable ที่พึ่งพา `onMounted` อย่างไร?** lifecycle hook จะทำงานเฉพาะภายใน instance ของคอมโพเนนต์ ดังนั้นจึงเรียก composable โดยตรงไม่ได้ เทคนิคมาตรฐานคือสร้างคอมโพเนนต์ harness แบบใช้แล้วทิ้งที่เรียก composable ใน `setup` และเปิดค่าที่คืนออกมา จากนั้นจึง mount harness นั้นด้วย Vue Test Utils แล้วยืนยันผลผ่าน wrapper วิธีนี้ทำให้การเชื่อมต่อ reactivity และ lifecycle ยังคงสมบูรณ์ในขณะที่ยังแยกตรรกะที่กำลังทดสอบให้อิสระ คำถามเชิงแนวคิดและเชิงเขียนโค้ดเพิ่มเติมรวบรวมไว้ในคู่มือ[คำถามสัมภาษณ์ Vue.js ที่จำเป็น](/blog/vue-nuxt/essential-vuejs-interview-questions) ## บทสรุป การตั้งค่าการทดสอบ Vue ที่เชื่อถือได้ในปี 2026 สรุปลงเหลือทางเลือกที่พิจารณาอย่างตั้งใจไม่กี่ข้อ: - รัน Vitest ด้วย `environment: 'jsdom'` และปลั๊กอิน Vue เพื่อให้เทสต์ใช้ transform pipeline ของ Vite ชุดเดียวกับตัวแอปเป๊ะ - เลือก `mount` มากกว่า `shallowMount` เว้นแต่คอมโพเนนต์ลูกจะหนักหรือถูกครอบคลุมไว้แล้วที่อื่น - `await` `trigger`, `setValue` และ `nextTick` เสมอก่อนยืนยันผล เพื่อกำจัดเทสต์ที่ผลลัพธ์ไม่แน่นอนและขึ้นกับจังหวะเวลา - ทดสอบ composable ที่ใช้เพียง reactivity API ด้วยการเรียกมันโดยตรงและยืนยันผลบน `.value` - mock ที่ขอบเขตของโมดูลด้วย `vi.mock` เพื่อให้พฤติกรรมเครือข่ายคงเส้นคงวาและทดสอบเส้นทาง error ได้ - ใช้ `createTestingPinia({ createSpy: vi.fn })` สำหรับเทสต์คอมโพเนนต์ และ `stubActions: false` เมื่อต้องทดสอบตรรกะของ store - query element ผ่านแอตทริบิวต์ `data-test` เพื่อให้เทสต์ล้มเฉพาะเมื่อพฤติกรรมจริงเปลี่ยน ไม่ใช่เพราะการปรับสไตล์ เมื่อเชี่ยวชาญแพตเทิร์นเหล่านี้ การทดสอบจะกลายเป็นเครื่องมือออกแบบมากกว่าภาระ ที่คอยดักจับ regression ก่อนจะหลุดไปถึงโปรดักชัน สำรวจ[เส้นทางการเรียนรู้ Vue และ Nuxt](/technologies/vue-nuxt) ฉบับเต็มเพื่อสร้างทักษะที่พร้อมสำหรับการสัมภาษณ์ต่อไป --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/vue-nuxt/vue-3-testing-vitest-vue-test-utils