# Kiểm thử Vue 3 năm 2026: Vitest, Vue Test Utils và câu hỏi phỏng vấn > Hướng dẫn thực hành về kiểm thử Vue năm 2026: cấu hình Vitest, mount component với Vue Test Utils, kiểm thử composable và Pinia store, mock API, đo độ phủ và những câu hỏi phỏng vấn mà đội tuyển dụng thường đặt ra. - Published: 2026-06-21 - Updated: 2026-07-06 - Author: SharpSkill - Tags: vue, testing, vitest, vue-test-utils, interview - Reading time: 10 min --- Kiểm thử Vue là ranh giới phân tách giữa việc tái cấu trúc một cách tự tin và việc phỏng đoán mong manh, và đến năm 2026, bộ công cụ đã hội tụ quanh hai thư viện chủ đạo: [Vitest](https://vitest.dev/guide/) đóng vai trò trình chạy kiểm thử và [Vue Test Utils](https://test-utils.vuejs.org/) để mount component. Bài viết này sẽ trình bày cách cấu hình toàn bộ hệ công cụ, kiểm thử component và composable, mock các phụ thuộc, cùng lời giải cho những câu hỏi phỏng vấn về kiểm thử Vue mà đội tuyển dụng thực sự hay hỏi. > **Bộ công cụ kiểm thử Vue hiện đại năm 2026** > > Khuyến nghị mặc định từ [tài liệu Vue](https://vuejs.org/guide/scaling-up/testing.html) là dùng Vitest cho kiểm thử đơn vị và kiểm thử component, Vue Test Utils làm API mount cấp thấp, còn Playwright hoặc Cypress để phủ kiểm thử đầu-cuối. Vitest tái sử dụng chính cấu hình Vite của ứng dụng, nhờ đó các bài kiểm thử chạy qua đúng pipeline biến đổi đó với khả năng nạp lại gần như tức thì. ## Cấu hình Vitest cho dự án Vue 3 Vitest dùng chung pipeline của Vite, nghĩa là một tệp cấu hình duy nhất điều khiển cả máy chủ phát triển lẫn trình chạy kiểm thử. Tùy chọn `environment` phải được đặt thành `jsdom` hoặc `happy-dom` để các bài kiểm thử component có một DOM để dựng vào. Cờ `globals: true` phơi bày `describe`, `it` và `expect` mà không cần import chúng trong mỗi tệp. ```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)), }, }, }) ``` Với Vitest 3, cấu hình trên chạy được ngay sau khi cài `vitest`, `@vue/test-utils`, `@vitejs/plugin-vue` và `jsdom`. Do alias phản chiếu đúng cấu hình của ứng dụng, các câu lệnh import như `@/composables/useCart` được phân giải y hệt nhau trong cả kiểm thử lẫn môi trường sản xuất. ## Mount component với Vue Test Utils Vue Test Utils cung cấp hai điểm vào: `mount` dựng toàn bộ cây component, và `shallowMount` thay thế các component con bằng bản rút gọn (stub). Với phần lớn kiểm thử đơn vị, `mount` được ưu tiên vì nó vận hành hành vi dựng hình thực tế. Đối tượng `wrapper` trả về cung cấp các hàm truy vấn như `find`, `get` và `text` để kiểm chứng kết quả đầu ra đã dựng. ```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') }) }) ``` Việc nhắm đến các phần tử qua thuộc tính `data-test` thay vì qua lớp CSS giúp bài kiểm thử bền vững hơn: một thay đổi về kiểu dáng trên `.price--sale` sẽ không phá vỡ bộ chọn, chỉ có thay đổi thực sự về hành vi mới làm điều đó. Sự tách bạch này là một chủ đề lặp đi lặp lại trong kiểm thử Vue dễ bảo trì. ## Kiểm thử sự kiện người dùng và sự kiện phát ra Các component tương tác cần được kiểm chứng về những gì xảy ra sau một cú nhấp hoặc một lần nhập liệu. Vue Test Utils trả về một promise từ `trigger`, và việc await promise đó sẽ xả hàng đợi phản ứng (reactivity) của Vue để DOM phản ánh cập nhật. Hàm `emitted` ghi lại mọi sự kiện tùy chỉnh mà component đã phát ra, đây chính là cách kiểm chứng hợp đồng giữa component cha và con. ```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') }) }) ``` > **Luôn await các cập nhật DOM** > > Vue gộp các cập nhật phản ứng theo cơ chế bất đồng bộ. Quên `await` một lời gọi `trigger` hay `setValue` là nguyên nhân phổ biến nhất gây ra các bài kiểm thử Vue chập chờn: phần kiểm chứng chạy trước khi DOM dựng lại, nên nó đọc phải kết quả đã cũ. Khi một thay đổi trạng thái xảy ra bên ngoài các hàm trợ giúp sự kiện, hãy import `nextTick` từ `vue` và `await nextTick()` trước khi kiểm chứng. ## Kiểm thử Vue composable một cách độc lập Những composable chỉ dùng các API phản ứng (`ref`, `computed`, `watch`) hoàn toàn không cần đến component: chúng là những hàm thuần trả về trạng thái phản ứng, nên chỉ cần gọi trực tiếp và kiểm chứng trên `.value` là đủ. Đây là cách nhanh nhất và tập trung nhất để phủ phần logic nghiệp vụ, và nó ăn khớp với các mẫu hình trong [composable Vue 3 nâng cao](/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) }) }) ``` Những composable phụ thuộc vào các lifecycle hook như `onMounted` là trường hợp ngoại lệ: chúng buộc phải chạy bên trong một thực thể component. Cách xử lý thông dụng là dựng một component trợ giúp nhỏ gọi composable trong `setup` rồi phơi bày kết quả, sau đó mount component trợ giúp đó bằng Vue Test Utils. ## Mock lời gọi API bằng vi.mock Những yêu cầu mạng thật khiến bài kiểm thử chậm và thiếu tính tất định. Vitest thay thế module bằng `vi.mock` và tạo các hàm gián điệp (spy) bằng `vi.fn`. Mẫu bên dưới mock module bọc quanh `fetch`, nhờ đó composable đang được kiểm thử nhận về dữ liệu có kiểm soát mà không hề chạm tới máy chủ. ```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') }) }) ``` Việc mock tại ranh giới module giữ nguyên vẹn logic bên trong của composable trong khi trao cho mỗi bài kiểm thử toàn quyền kiểm soát phản hồi của API, bao gồm cả các nhánh lỗi. Dùng `mockRejectedValue` thay thế cho phép một bài kiểm thử riêng lẻ xác minh rằng thất bại sẽ thiết lập trạng thái lỗi và dừng vòng xoay tải. ## Kiểm thử Pinia store năm 2026 Store giữ trạng thái dùng chung giữa nhiều component, và gói chính thức `@pinia/testing` khiến việc kiểm thử chúng trở nên đơn giản. `createTestingPinia` mặc định thay thế mọi action bằng bản rút gọn, nhờ đó một bài kiểm thử component có thể kiểm chứng rằng một action đã được điều phối mà không cần chạy các tác dụng phụ của nó. [Hướng dẫn kiểm thử Pinia](https://pinia.vuejs.org/cookbook/testing.html) khuyến nghị cách tiếp cận này cho các bài kiểm thử ở cấp component. ```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() }) }) ``` Để kiểm thử chính bản thân logic của store thay vì sự tích hợp với component, hãy truyền `stubActions: false` để các action chạy bình thường, rồi kiểm chứng trên trạng thái thu được. Sự phân chia giữa kiểm thử component với action bị rút gọn và kiểm thử store với action thật giúp mỗi bộ kiểm thử tập trung vào một trách nhiệm duy nhất. ## Chạy kiểm thử ở chế độ theo dõi và đo độ phủ Vitest đi kèm chế độ theo dõi (watch mode) chỉ chạy lại những bài kiểm thử chịu ảnh hưởng bởi tệp vừa lưu, nhờ đó rút ngắn vòng phản hồi trong quá trình phát triển xuống chỉ còn một phần nhỏ của giây. Việc báo cáo độ phủ được tích hợp sẵn qua provider `@vitest/coverage-v8` và ghép nối gọn gàng vào pipeline tích hợp liên tục, nơi một bài kiểm thử thất bại hoặc một sự suy giảm độ phủ có thể chặn việc gộp mã. ```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/**'], }, }, }) ``` Chạy riêng lệnh `vitest` sẽ khởi động chế độ theo dõi trên máy cục bộ, trong khi `vitest run --coverage` thực thi một lượt chạy duy nhất phù hợp với CI. Việc đặt ngưỡng rõ ràng biến độ phủ từ một chỉ số phù phiếm thành một cổng chất lượng được thực thi: một pull request làm độ phủ nhánh rơi xuống dưới 75 phần trăm sẽ tự động thất bại, qua đó thúc đẩy người đóng góp kiểm thử cả những nhánh mà họ thêm vào chứ không chỉ mỗi kịch bản thuận lợi. ## Những câu hỏi phỏng vấn thường gặp về kiểm thử Vue và lời giải Câu hỏi về kiểm thử xuất hiện trong hầu hết các buổi phỏng vấn Vue cấp cao bởi chúng bộc lộ cách ứng viên suy nghĩ về khả năng bảo trì. Các lời giải bên dưới bao quát những khái niệm hay gặp nhất, và trọn bộ được luyện tập trong [module câu hỏi phỏng vấn về kiểm thử Vue](/technologies/vue-nuxt/interview-questions/vue-testing). **Khi nào nên dùng `shallowMount` thay cho `mount`?** Hãy chọn `shallowMount` khi một component dựng ra các con nặng nề hoặc đã được kiểm thử ở nơi khác, và bài kiểm thử chỉ quan tâm đến logic riêng của component cha. Việc rút gọn các con giúp cô lập đơn vị và tăng tốc dựng hình, đổi lại là không kiểm chứng được sự tích hợp thực tế giữa các component. **Các cập nhật bất đồng bộ được xử lý ra sao trong Vue Test Utils?** Vue áp dụng các thay đổi phản ứng ở tick tiếp theo. Những hàm trợ giúp sự kiện như `trigger` và `setValue` trả về promise được giải quyết sau khi DOM cập nhật, nên chỉ cần await chúng là đủ. Với trạng thái thay đổi bên ngoài các hàm đó, `await nextTick()` buộc hàng đợi phải được xả trước khi phần kiểm chứng chạy. **Khác biệt giữa `find` và `get` là gì?** `find` trả về một wrapper rỗng khi không có phần tử nào khớp và không bao giờ ném lỗi, phù hợp với các kiểm chứng như `expect(wrapper.find('.error').exists()).toBe(false)`. Còn `get` ném lỗi ngay lập tức khi phần tử vắng mặt, trở thành lựa chọn tốt hơn khi phần tử được kỳ vọng tồn tại và sự vắng mặt của nó là một thất bại thực sự. **Vì sao nên ưu tiên thuộc tính `data-test` hơn bộ chọn theo lớp?** Tên lớp thay đổi theo kiểu dáng và cấu trúc, nên những bài kiểm thử truy vấn chúng sẽ đổ vỡ trước các chỉnh sửa mang tính thẩm mỹ. Một điểm neo `data-test` chuyên dụng thể hiện rõ ý đồ kiểm thử và sống sót qua các lần tái cấu trúc, giảm bớt các cảnh báo sai. **Làm thế nào để kiểm thử một composable phụ thuộc vào `onMounted`?** Các lifecycle hook chỉ kích hoạt bên trong một thực thể component, nên không thể gọi trực tiếp composable. Kỹ thuật tiêu chuẩn là dựng một component vỏ dùng một lần rồi bỏ, nó gọi composable trong `setup` và phơi bày giá trị trả về, sau đó mount component vỏ này bằng Vue Test Utils và kiểm chứng thông qua wrapper. Cách này giữ nguyên vẹn liên kết phản ứng và vòng đời trong khi vẫn cô lập được phần logic cần kiểm thử. Nhiều câu hỏi mang tính khái niệm và lập trình hơn được tập hợp trong hướng dẫn [những câu hỏi phỏng vấn Vue.js thiết yếu](/blog/vue-nuxt/essential-vuejs-interview-questions). ## Kết luận Một thiết lập kiểm thử Vue đáng tin cậy vào năm 2026 quy về vài lựa chọn có chủ đích: - Chạy Vitest với `environment: 'jsdom'` cùng plugin Vue để các bài kiểm thử dùng chung đúng pipeline biến đổi Vite của ứng dụng. - Ưu tiên `mount` hơn `shallowMount` trừ khi một component con nặng nề hoặc đã được phủ ở nơi khác. - Luôn `await` `trigger`, `setValue` và `nextTick` trước khi kiểm chứng để loại bỏ các bài kiểm thử chập chờn phụ thuộc thời điểm. - Kiểm thử những composable chỉ dùng API phản ứng bằng cách gọi trực tiếp và kiểm chứng trên `.value`. - Mock tại ranh giới module bằng `vi.mock` để hành vi mạng có tính tất định và các nhánh lỗi kiểm thử được. - Dùng `createTestingPinia({ createSpy: vi.fn })` cho kiểm thử component và `stubActions: false` khi vận hành logic của store. - Truy vấn phần tử qua thuộc tính `data-test` để bài kiểm thử chỉ đổ vỡ trước những thay đổi hành vi thực sự, không bao giờ vì kiểu dáng. Làm chủ những mẫu hình này thì kiểm thử sẽ trở thành một công cụ thiết kế thay vì một việc vặt nhàm chán, bắt được các hồi quy trước khi chúng lọt ra môi trường sản xuất. Hãy khám phá trọn vẹn [lộ trình học Vue và Nuxt](/technologies/vue-nuxt) để tiếp tục bồi đắp những kỹ năng sẵn sàng cho phỏng vấn. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/vue-nuxt/vue-3-testing-vitest-vue-test-utils