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.

Quy trình kiểm thử Vue với Vitest và Vue Test Utils năm 2026

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 đóng vai trò trình chạy kiểm thử và Vue Test Utils để 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 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, itexpect mà không cần import chúng trong mỗi tệp.

vitest.config.tstypescript
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-vuejsdom. 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, gettext để kiểm chứng kết quả đầu ra đã dựng.

PriceTag.spec.tstypescript
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.

SearchBar.spec.tstypescript
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ừ vueawait 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.

useCart.spec.tstypescript
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.

Sẵn sàng chinh phục phỏng vấn Vue.js / Nuxt.js?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

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ủ.

useProducts.spec.tstypescript
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 khuyến nghị cách tiếp cận này cho các bài kiểm thử ở cấp component.

CheckoutButton.spec.tstypescript
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ã.

vitest.config.ts (coverage section)typescript
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.

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ư triggersetValue 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 findget 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.

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, setValuenextTick 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 để tiếp tục bồi đắp những kỹ năng sẵn sàng cho phỏng vấn.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

#vue
#testing
#vitest
#vue-test-utils
#interview

Chia sẻ

Bài viết liên quan