Vitest 4 — Testing Framework nhanh gấp 28 lần Jest cho Vue và Vite

Posted on: 4/26/2026 1:13:24 PM

28x Nhanh hơn Jest (watch mode)
5.6x Cold start nhanh hơn
57% Ít bộ nhớ hơn
4.0 Phiên bản stable mới nhất

1. Vitest là gì và tại sao quan trọng

Vitest là testing framework thế hệ mới được xây dựng trên nền tảng Vite — build tool đang thống trị hệ sinh thái frontend. Thay vì phải cấu hình phức tạp với babel, ts-jest, và hàng loạt plugin như khi dùng Jest, Vitest tận dụng trực tiếp pipeline transform của Vite: TypeScript, JSX, CSS Modules đều hoạt động ngay lập tức mà không cần thêm config.

Với bản 4.0 phát hành năm 2026, Vitest đánh dấu bước ngoặt quan trọng khi Browser Mode chính thức stable, Visual Regression Testing được tích hợp sẵn, và Playwright Traces giúp debug test thất bại trực quan hơn bao giờ hết.

Tại sao chuyển từ Jest sang Vitest?

Jest được thiết kế cho thời kỳ CommonJS. Dù Jest 30 đã cải thiện ESM support, việc cấu hình vẫn phức tạp — cần ts-jest hoặc babel-jest cho TypeScript, moduleNameMapper cho path aliases, và mock CSS riêng. Vitest kế thừa toàn bộ config từ vite.config.ts, nên mọi alias, plugin, transform đều hoạt động nhất quán giữa dev server và test runner.

2. Kiến trúc bên trong Vitest

Vitest không phải đơn thuần là "Jest chạy trên Vite". Kiến trúc của nó được thiết kế lại từ đầu để tận dụng tối đa ESM nativeHot Module Replacement:

graph TB
    A["vite.config.ts
Shared Config"] --> B["Vite Dev Server
Transform Pipeline"] B --> C["Vitest Runner
Test Scheduler"] C --> D["Worker Pool
tinypool threads"] C --> E["Browser Mode
Playwright / WebdriverIO"] D --> F["Node.js Tests
Unit & Integration"] E --> G["Real Browser Tests
Component & E2E"] B --> H["HMR Watcher
File Change Detection"] H --> C style A fill:#e94560,stroke:#fff,color:#fff style B fill:#2c3e50,stroke:#fff,color:#fff style C fill:#2c3e50,stroke:#fff,color:#fff style D fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style E fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style F fill:#f8f9fa,stroke:#e0e0e0,color:#555 style G fill:#f8f9fa,stroke:#e0e0e0,color:#555 style H fill:#16213e,stroke:#fff,color:#fff

Kiến trúc Vitest: tận dụng Vite transform pipeline và HMR cho watch mode cực nhanh

Khi bạn thay đổi một file, Vite HMR xác định chính xác module nào bị ảnh hưởng. Vitest chỉ chạy lại những test liên quan đến module đó — không cần re-parse toàn bộ dependency graph như Jest. Đây là lý do watch mode nhanh gấp 28 lần.

3. Vitest 4.0 — Tính năng mới

3.1. Browser Mode chính thức stable

Sau nhiều phiên bản experimental, Browser Mode trong Vitest 4.0 đã thoát khỏi trạng thái thử nghiệm. Thay vì test component trong jsdom (môi trường giả lập DOM), bạn chạy test trong trình duyệt thật thông qua Playwright hoặc WebdriverIO:

// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      instances: [
        { browser: 'chromium' },
        { browser: 'firefox' },
        { browser: 'webkit' }
      ]
    }
  }
})

Các provider giờ được tách thành package riêng biệt:

  • @vitest/browser-playwright — cho Playwright
  • @vitest/browser-webdriverio — cho WebdriverIO
  • @vitest/browser-preview — cho preview mode

3.2. Visual Regression Testing tích hợp sẵn

Vitest 4.0 giới thiệu assertion toMatchScreenshot — cho phép so sánh ảnh chụp màn hình trực tiếp trong test mà không cần thư viện bên ngoài:

import { test, expect } from 'vitest'
import { page } from 'vitest/browser'

test('hero section renders correctly', async () => {
  await page.goto('/landing')

  await expect(
    page.getByTestId('hero-section')
  ).toMatchScreenshot('hero-section')
})

test('element is visible in viewport', async () => {
  await expect(
    page.getByRole('banner')
  ).toBeInViewport({ ratio: 0.8 })
})

Lợi ích thực tế

Visual Regression Testing phát hiện những thay đổi UI mà unit test không thể bắt được: font bị thay, spacing lệch 2px, màu sắc sai do CSS specificity. Trước Vitest 4, bạn phải dùng tool riêng như Percy, Chromatic hoặc cấu hình phức tạp với jest-image-snapshot.

3.3. Playwright Traces

Khi test browser thất bại, debug thường rất khó khăn vì không biết trạng thái DOM tại thời điểm fail. Vitest 4.0 tích hợp Playwright Traces — ghi lại toàn bộ timeline tương tác:

# Bật trace cho lần retry đầu tiên
npx vitest --browser.trace=on-first-retry

# Bật trace cho mọi lần retry
npx vitest --browser.trace=on-all-retries

# Chỉ giữ trace khi test fail
npx vitest --browser.trace=retain-on-failure

Traces xuất hiện dưới dạng annotation trong reporter, và bạn có thể mở bằng Playwright Trace Viewer để xem từng bước: click, navigation, network request, DOM snapshot.

3.4. Schema Matching với Zod, Valibot, ArkType

Tính năng mới expect.schemaMatching cho phép validate dữ liệu trả về theo Standard Schema v1:

import { z } from 'zod'
import { expect, test } from 'vitest'

const UserSchema = z.object({
  id: z.number().positive(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'editor'])
})

test('API returns valid user', async () => {
  const response = await fetch('/api/user/1')
  const user = await response.json()

  expect(user).toEqual({
    id: expect.any(Number),
    email: expect.schemaMatching(z.string().email()),
    role: expect.schemaMatching(z.enum(['admin', 'user', 'editor']))
  })
})

3.5. Type-Aware Fixtures

Vitest 4.0 mở rộng test fixtures với type-safe lifecycle hooks:

import { test as base } from 'vitest'

const test = base.extend<{ todos: string[] }>({
  todos: async ({}, use) => {
    const list: string[] = []
    await use(list)
    // cleanup after test
  }
})

// beforeEach có đầy đủ type inference
test.beforeEach(({ todos }) => {
  todos.push('initial item')
})

test('add todo', ({ todos }) => {
  todos.push('new item')
  expect(todos).toHaveLength(2)
})

4. Benchmark: Vitest vs Jest — Số liệu thực tế

Benchmark được thực hiện trên cùng bộ 500 test files, chạy trên Node.js 22, máy 16GB RAM:

Tiêu chí Vitest 3/4 Jest 30 Chênh lệch
Cold start (500 files) ~38 giây ~214 giây 5.6x nhanh hơn
Watch mode re-run ~0.3 giây ~8.4 giây 28x nhanh hơn
Peak memory ~400 MB ~930 MB 57% ít hơn
Native ESM Hỗ trợ một phần
TypeScript Native qua Vite Cần ts-jest/babel Zero config
Browser testing Playwright/WebdriverIO Chỉ jsdom Real browser
CSS Modules Tự động qua Vite Cần moduleNameMapper Zero config
graph LR
    subgraph Jest["Jest 30"]
        J1["Source File"] --> J2["babel-jest
Transform"] J2 --> J3["ts-jest
TypeScript"] J3 --> J4["moduleNameMapper
Aliases"] J4 --> J5["jsdom
DOM Simulation"] end subgraph Vitest["Vitest 4"] V1["Source File"] --> V2["Vite Pipeline
TS + JSX + CSS"] V2 --> V3["Real Browser
or Node.js"] end style J1 fill:#f8f9fa,stroke:#e0e0e0,color:#555 style J2 fill:#f8f9fa,stroke:#e0e0e0,color:#555 style J3 fill:#f8f9fa,stroke:#e0e0e0,color:#555 style J4 fill:#f8f9fa,stroke:#e0e0e0,color:#555 style J5 fill:#f8f9fa,stroke:#e0e0e0,color:#555 style V1 fill:#e94560,stroke:#fff,color:#fff style V2 fill:#e94560,stroke:#fff,color:#fff style V3 fill:#e94560,stroke:#fff,color:#fff

Transform pipeline: Jest cần nhiều lớp plugin, Vitest dùng trực tiếp Vite

5. Tích hợp Vitest với Vue 3

5.1. Setup cơ bản

# Cài đặt
npm install -D vitest @vue/test-utils @vitejs/plugin-vue happy-dom
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'happy-dom',
    globals: true,
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov']
    }
  }
})

5.2. Component Testing

// TodoList.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import TodoList from './TodoList.vue'

describe('TodoList', () => {
  it('renders todos from props', () => {
    const wrapper = mount(TodoList, {
      props: {
        items: ['Learn Vitest', 'Write tests']
      }
    })

    expect(wrapper.findAll('li')).toHaveLength(2)
    expect(wrapper.text()).toContain('Learn Vitest')
  })

  it('emits add event on form submit', async () => {
    const wrapper = mount(TodoList, {
      props: { items: [] }
    })

    await wrapper.find('input').setValue('New todo')
    await wrapper.find('form').trigger('submit')

    expect(wrapper.emitted('add')).toHaveLength(1)
    expect(wrapper.emitted('add')![0]).toEqual(['New todo'])
  })
})

5.3. Testing Composables

// useCounter.test.ts
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('increments count', () => {
    const { count, increment } = useCounter()

    expect(count.value).toBe(0)
    increment()
    expect(count.value).toBe(1)
  })

  it('respects max limit', () => {
    const { count, increment } = useCounter({ max: 2 })

    increment()
    increment()
    increment() // should not exceed max
    expect(count.value).toBe(2)
  })
})

5.4. Testing Pinia Stores

// userStore.test.ts
import { setActivePinia, createPinia } from 'pinia'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { useUserStore } from './userStore'

describe('User Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('fetches user profile', async () => {
    const store = useUserStore()

    // Mock fetch API
    vi.spyOn(globalThis, 'fetch').mockResolvedValue(
      new Response(JSON.stringify({
        name: 'Anh Tú',
        role: 'admin'
      }))
    )

    await store.fetchProfile()

    expect(store.user?.name).toBe('Anh Tú')
    expect(store.isAdmin).toBe(true)
  })
})

6. Browser Mode — Test Component trong trình duyệt thật

Khác biệt lớn nhất giữa jsdom và Browser Mode: jsdom không có layout engine. Các API như getBoundingClientRect(), IntersectionObserver, ResizeObserver, CSS animations đều không hoạt động đúng trong jsdom. Browser Mode giải quyết hoàn toàn vấn đề này.

// vitest.config.ts — Browser Mode cho Vue
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    browser: {
      enabled: true,
      instances: [
        { browser: 'chromium', provider: 'playwright' }
      ]
    }
  }
})
// Dialog.browser.test.ts
import { test, expect } from 'vitest'
import { page } from 'vitest/browser'
import { render } from 'vitest-browser-vue'
import Dialog from './Dialog.vue'

test('dialog has correct position and animation', async () => {
  const screen = render(Dialog, {
    props: { open: true, title: 'Confirm' }
  })

  const dialog = screen.getByRole('dialog')

  // Real browser APIs work!
  await expect(dialog).toBeInViewport({ ratio: 1.0 })

  // Visual regression test
  await expect(dialog).toMatchScreenshot('dialog-open')
})

Lưu ý quan trọng

Browser Mode yêu cầu cài đặt Playwright: npx playwright install. Trong CI, thêm step cài browser vào pipeline. Browser tests chậm hơn unit test thuần Node.js, nên chỉ dùng cho component thật sự cần DOM thật (animation, layout, visual regression).

7. Migration từ Jest sang Vitest

Vitest thiết kế API tương thích gần như hoàn toàn với Jest. Quá trình migration thường chỉ mất vài bước:

Bước 1: Cài đặt
npm install -D vitest và xóa jest, ts-jest, babel-jest, @types/jest khỏi dependencies.
Bước 2: Config
Xóa jest.config.js. Thêm block test vào vite.config.ts hoặc tạo vitest.config.ts. Không cần moduleNameMapper — Vite aliases tự động hoạt động.
Bước 3: Import
Thay jest.fn()vi.fn(), jest.mock()vi.mock(), jest.spyOn()vi.spyOn(). Hoặc bật globals: true để dùng không cần import.
Bước 4: Scripts
Thay "test": "jest""test": "vitest" trong package.json. Thêm "test:ui": "vitest --ui" để có giao diện web đẹp.
Bước 5: Verify
Chạy npx vitest run. Snapshot format tương thích Jest nên không cần update. Coverage report chuyển sang v8 (nhanh hơn istanbul).
// Thay đổi import duy nhất cần thiết:
- import { jest } from '@jest/globals'
+ import { vi } from 'vitest'

// API gần như giống hệt:
- const mockFn = jest.fn()
+ const mockFn = vi.fn()

- jest.spyOn(api, 'fetchUser')
+ vi.spyOn(api, 'fetchUser')

- jest.useFakeTimers()
+ vi.useFakeTimers()

8. Tích hợp CI/CD

Vitest hỗ trợ nhiều reporter format cho CI:

# .github/workflows/test.yml
name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - run: npm ci

      # Unit tests
      - run: npx vitest run --reporter=junit --outputFile=results.xml

      # Coverage
      - run: npx vitest run --coverage

      # Browser tests (cần cài Playwright)
      - run: npx playwright install --with-deps chromium
      - run: npx vitest run --project=browser

Workspace Mode cho Monorepo

Vitest 4.0 cho phép cấu hình workspace trực tiếp trong vitest.config.ts thay vì file riêng:

export default defineConfig({
  test: {
    workspace: [
      { test: { name: 'unit', environment: 'node' } },
      { test: { name: 'browser', browser: { enabled: true } } }
    ]
  }
})

9. Best Practices khi dùng Vitest

Practice Nên làm Tránh
Naming *.test.ts hoặc *.spec.ts cạnh source file Folder __tests__ tách riêng xa source
Mocking vi.mock() cho external modules, vi.spyOn() cho method Mock mọi thứ — test mất ý nghĩa
Environment happy-dom (nhanh hơn jsdom 2-3x) jsdom khi không cần full browser compat
Coverage Provider v8 (native, nhanh) istanbul (chậm hơn, cần instrument)
Browser tests Chỉ cho component cần DOM thật Mọi test đều browser — chậm CI

10. Kết luận

Vitest 4.0 không chỉ là "Jest nhanh hơn" — nó là testing framework được thiết kế cho hệ sinh thái hiện đại với ESM native, TypeScript zero-config, và Browser Mode thật sự hoạt động. Với Vue 3, Nuxt 4, và Vite đã trở thành tiêu chuẩn, Vitest là lựa chọn tự nhiên và hiệu quả nhất cho testing.

Nếu bạn đang bắt đầu dự án mới, Vitest là default không cần suy nghĩ. Nếu đang dùng Jest, quá trình migration chỉ mất 30 phút cho hầu hết dự án — và bạn sẽ ngay lập tức cảm nhận được sự khác biệt với watch mode gần như tức thì.

Nguồn tham khảo:
Vitest 4.0 Official Blog · Vitest 3 vs Jest 30: Testing in 2026 — PkgPulse · Vitest in 2026 — DEV Community · Vitest GitHub Repository