Kiểm thử React hiện đại với Vitest và React Testing Library

Table of Contents
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ẽ đi sâu vào 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.
Tại sao phải di chuyển từ Jest sang Vitest?
Trước khi đi sâu vào 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.
- Hỗ trợ ESM gốc: Jest trước đây đã gặp khó khăn với ESM, thường yêu cầu cấu hình Babel phức tạp hoặc các cờ thử nghiệm. Vitest hỗ trợ ESM ngay lập tức.
- Tích hợp Vite: Nếu ứng dụng React của bạn đã sử dụng Vite, Vitest chia sẻ cùng cấu hình (
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". - Tốc độ: Bằng cách tận dụng ESBuild để chuyển đổi, Vitest thực thi các bài kiểm thử nhanh hơn đáng kể so với Jest, đặc biệt là trong các codebase lớn.
- Chế độ Watch: Chế độ watch thông minh của Vitest (được cung cấp bởi cơ chế HMR của Vite) ngay lập tức chạy lại chỉ những bài kiểm thử bị ảnh hưởng bởi các thay đổi tệp.
- Khả năng tương thích API: Vitest cung cấp API tương thích với Jest, giúp việc di chuyển trở nên đơn giản đáng ngạc nhiên.
describe,it,expectvàvi(tương đương vớijestcủa Vitest) hầu hết là các thay thế trực tiếp.
Bước 1: Cài đặt và Cấu hình
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'),
},
},
});
Thiết lập Môi trường
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.
Bước 2: Quá trình Di chuyển (Jest sang Vitest)
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: Nếu bạn đặt
globals: truetrong cấu hình của mình, bạn không cần importdescribe,ithoặcexpecttừvitest. - Mocking: Thay thế
jest.fn()bằngvi.fn(),jest.spyOn()bằngvi.spyOn(), vàjest.mock()bằngvi.mock(). - Timers: Thay thế
jest.useFakeTimers()bằngvi.useFakeTimers().
Ví dụ Mocking Nâng cao
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.
Bước 3: Kiểm thử Component Hiện đại với React Testing Library
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>
);
};
Viết bài kiểm thử
Đâ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');
});
});
Những điểm chính rút ra từ bộ kiểm thử:
userEventthay 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ộtgetBy*trong một khốiwaitFor, RTL hiện đại khuyến khích sử dụngawait 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.
Tích hợp liên tục và Độ bao phủ
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.
Kết luận
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ẻ!
Bạn cũng có thể thích
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Sự thay đổi mô hình của React Server Components
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.
Read more
Di chuyển từ Jest sang Vitest trong kiến trúc Turborepo Monorepo
Hướng dẫn di chuyển từng bước hoàn chỉnh để chuyển đổi các bộ kiểm thử Jest sang Vitest trong Turborepo monorepo TypeScript với thời gian thực thi nhanh hơn 10 lần.
Read more
Ngừng sử dụng fireEvent: Hướng dẫn React Testing user-event v14
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.
Read more