Vitest 4 — Testing Framework nhanh gấp 28 lần Jest cho Vue và Vite
Posted on: 4/26/2026 1:13:24 PM
Table of contents
- 1. Vitest là gì và tại sao quan trọng
- 2. Kiến trúc bên trong Vitest
- 3. Vitest 4.0 — Tính năng mới
- 4. Benchmark: Vitest vs Jest — Số liệu thực tế
- 5. Tích hợp Vitest với Vue 3
- 6. Browser Mode — Test Component trong trình duyệt thật
- 7. Migration từ Jest sang Vitest
- 8. Tích hợp CI/CD
- 9. Best Practices khi dùng Vitest
- 10. Kết luận
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 native và Hot 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 | Có | 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:
npm install -D vitest và xóa jest, ts-jest, babel-jest, @types/jest khỏi dependencies.
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.
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.
"test": "jest" → "test": "vitest" trong package.json. Thêm "test:ui": "vitest --ui" để có giao diện web đẹp.
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
Progressive Web Apps 2026: Offline-First với Service Worker, Workbox và Vue.js
Cloudflare D1 — Serverless SQL Database chạy trên Edge
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.