
Việc kiểm thử ứng dụng React đã phát triển đáng kể qua nhiều năm. Trong một thời gian dài, Jest là ông vua không thể tranh cãi của hệ sinh thái kiểm thử JavaScript. Tuy nhiên, khi ngành công nghiệp chuyển sang ES Modules (ESM) và các công cụ build hiện đại như Vite, Jest bắt đầu bộc lộ những hạn chế của mình - đặc biệt là về tốc độ thực thi và khả năng tương thích ESM. Vitest xuất hiện, một framework kiểm thử cực nhanh, tích hợp sâu với Vite, đang nhanh chóng trở thành tiêu chuẩn mới cho các dự án React.
Trong hướng dẫn toàn diện này, chúng ta sẽ thực hiện việc di chuyển từ Jest sang Vitest, tích hợp nó với React Testing Library (RTL), và tận dụng các mẫu kiểm thử hiện đại vượt trội hơn các hướng dẫn truyền thống.
Trước khi phân tích code, điều quan trọng là phải hiểu tại sao sự thay đổi này lại diễn ra. Mặc dù các hướng dẫn toàn diện (như của Robin Wieruch) trước đây đã ủng hộ Jest, nhưng bối cảnh frontend hiện đại đòi hỏi chu kỳ lặp lại nhanh hơn.
vite.config.ts), plugin và pipeline chuyển đổi. Điều này loại bỏ sự khác biệt đáng sợ "hoạt động trong dev, lỗi trong test".describe, it, expect và vi (tương đương với jest của Vitest) hầu hết là các thay thế trực tiếp.Hãy bắt đầu bằng cách loại bỏ Jest và cài đặt Vitest cùng với React Testing Library.
npm uninstall jest @types/jest ts-jest @testing-library/jest-dom
npm install -D vitest @vitest/ui @testing-library/react @testing-library/jest-dom jsdom @testing-library/user-event
Tiếp theo, cấu hình Vitest. Nếu bạn đang sử dụng Vite, chỉ cần thêm thuộc tính test vào vite.config.ts của bạn:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
globals: true, // Enables 'describe', 'it', 'expect' globally like Jest
environment: 'jsdom', // Simulates a browser environment for React components
setupFiles: './src/setupTests.ts', // Path to your setup file
css: true, // Process CSS imports
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
Tạo src/setupTests.ts để mở rộng expect của Vitest với các matcher DOM từ @testing-library/jest-dom:
// src/setupTests.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';
// Automatically unmount and cleanup DOM after the test is finished.
afterEach(() => {
cleanup();
});
// Mocking window.matchMedia if needed for UI libraries
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
Lưu ý chúng ta import @testing-library/jest-dom/vitest. Đây là một bổ sung mới hơn cung cấp các kiểu Vitest gốc, tránh xung đột namespace với Jest.
Di chuyển các tệp kiểm thử của bạn thường là một thao tác tìm kiếm và thay thế:
globals: true trong cấu hình của mình, bạn không cần import describe, it hoặc expect từ vitest.jest.fn() bằng vi.fn(), jest.spyOn() bằng vi.spyOn(), và jest.mock() bằng vi.mock().jest.useFakeTimers() bằng vi.useFakeTimers().Hãy xem cách mocking một module thay đổi.
Jest:
jest.mock('axios', () => ({
default: { get: jest.fn().mockResolvedValue({ data: {} }) }
}));
Vitest:
import { vi } from 'vitest';
import axios from 'axios';
vi.mock('axios', () => ({
default: { get: vi.fn().mockResolvedValue({ data: {} }) }
}));
Mặc dù có cấu trúc giống hệt nhau, cơ chế hoisting của Vitest hơi khác một chút. Vitest hoist vi.mock lên đầu tệp, giống như Jest, nhưng nó tích hợp liền mạch hơn với ESM, giảm các trường hợp ngoại lệ khi các module được mock đánh giá trước khi mock được áp dụng.
React Testing Library (RTL) khuyến khích kiểm thử các component từ góc độ người dùng thay vì kiểm thử các chi tiết triển khai. Hãy xây dựng một component bất đồng bộ phức tạp và kiểm thử nó một cách đúng đắn.
// src/components/UserProfile.tsx
import React, { useState, useEffect } from 'react';
interface User {
id: string;
name: string;
email: string;
}
export const UserProfile = ({ userId }: { userId: string }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
const data = await response.json();
setUser(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <div role="status">Loading...</div>;
if (error) return <div role="alert">{error}</div>;
if (!user) return <div>No user found</div>;
return (
<article>
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={() => console.log('Edit clicked')}>Edit Profile</button>
</article>
);
};
Đây là cách bạn viết một bộ kiểm thử hiện đại, bền vững cho component này bằng Vitest và RTL, tránh các anti-pattern phổ biến như sử dụng waitFor một cách không cần thiết.
// src/components/UserProfile.test.tsx
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
import { vi, describe, it, expect, beforeEach } from 'vitest';
// Mock the global fetch
global.fetch = vi.fn();
describe('UserProfile Component', () => {
const mockUser = { id: '1', name: 'Jane Doe', email: 'jane@example.com' };
beforeEach(() => {
vi.clearAllMocks();
});
it('renders loading state initially, then displays user data', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockUser,
});
render(<UserProfile userId="1" />);
// 1. Assert loading state
expect(screen.getByRole('status')).toHaveTextContent('Loading...');
// 2. Wait for loading to finish (Modern pattern instead of waitFor)
await waitForElementToBeRemoved(() => screen.queryByRole('status'));
// 3. Assert user data
expect(screen.getByRole('heading', { name: 'Jane Doe' })).toBeInTheDocument();
expect(screen.getByText('jane@example.com')).toBeInTheDocument();
});
it('renders error state on API failure', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: false,
});
render(<UserProfile userId="2" />);
// Wait for the alert role to appear
const alertElement = await screen.findByRole('alert');
expect(alertElement).toHaveTextContent('Failed to fetch user');
});
it('simulates user interaction correctly', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockUser,
});
const consoleSpy = vi.spyOn(console, 'log');
render(<UserProfile userId="1" />);
// Wait for data to load using findByRole
const editButton = await screen.findByRole('button', { name: /edit profile/i });
// Use userEvent instead of fireEvent for more realistic interactions
const user = userEvent.setup();
await user.click(editButton);
expect(consoleSpy).toHaveBeenCalledWith('Edit clicked');
});
});
userEvent thay vì fireEvent: Chúng tôi sử dụng @testing-library/user-event. Nó mô phỏng các sự kiện DOM đầy đủ (như focus bàn phím, bubbling, v.v.), làm cho các bài kiểm thử gần hơn đáng kể với cách người dùng thực tương tác với trình duyệt.findBy* thay vì waitFor: Thay vì bọc một getBy* trong một khối waitFor, RTL hiện đại khuyến khích sử dụng await screen.findBy*(). Nó sạch hơn và xử lý việc thăm dò nội bộ.waitForElementToBeRemoved: Đây là cách sạch nhất để khẳng định rằng một skeleton loading hoặc spinner đã biến mất trước khi tiếp tục khẳng định nội dung được render.Vitest sử dụng c8 (thông qua v8) hoặc istanbul để đo độ bao phủ mã. Để bật độ bao phủ, hãy chạy:
npx vitest run --coverage
Bạn có thể cần cài đặt nhà cung cấp: npm i -D @vitest/coverage-v8. Kết quả được tạo ra ngay lập tức, và vì nó chạy trên ESBuild, nó cực kỳ nhanh trong các pipeline CI.
Một đội phát triển đang di chuyển một ứng dụng React quy mô lớn từ Jest sang Vitest. Họ nhận thấy rằng sau khi di chuyển, giai đoạn kiểm thử của pipeline CI/CD của họ, trước đây mất 15 phút, giờ chỉ hoàn thành trong 5 phút. Tuy nhiên, một vài bài kiểm thử dựa vào các hành vi module mocking cụ thể của Jest đang bị lỗi. Đánh đổi có khả năng nhất mà họ đang gặp phải là gì?
Di chuyển từ Jest sang Vitest không chỉ là theo kịp xu hướng; đó là một nâng cấp thực tế cho trải nghiệm của nhà phát triển. Bạn có được hỗ trợ ESM gốc, cấu hình chia sẻ với Vite và tốc độ thực thi có thể giảm một nửa thời gian CI.
Khi kết hợp với cách tiếp cận triết lý của React Testing Library - kiểm thử hành vi thay vì chi tiết triển khai - bộ kiểm thử của bạn trở thành một mạng lưới an toàn mạnh mẽ thay vì một gánh nặng bảo trì dễ vỡ. Hãy đón nhận Vitest, sử dụng userEvent, và ngừng kiểm thử trạng thái nội bộ của các component. Chúc bạn kiểm thử vui vẻ!
Làm sạch log AI CLI, tạo biểu thức cron, giải mã JWT và tính quyền chmod offline.

Lý do bạn phải ngừng sử dụng fireEvent trong React Testing Library: Hướng dẫn đầy đủ về @testing-library/user-event v14, userEvent.setup(), async typing, accessible queries và MSW mocking.
Đọc tiếp
Khám phá cách React Server Components (RSC) thay đổi cơ bản cách chúng ta xây dựng ứng dụng React, mang lại kích thước bundle nhỏ hơn, đơn giản hóa việc tìm nạp dữ liệu và cải thiện hiệu suất.
Đọc tiếp
Tìm hiểu chuyên sâu về React Forget và React Compiler, bao gồm các biến đổi AST, phân tích luồng dữ liệu và lý do bạn cuối cùng có thể xóa useMemo.
Đọc tiếp