Ngừng sử dụng fireEvent: Hướng dẫn React Testing user-event v14

Table of Contents
Kiểm thử tự động là nền tảng của việc phát triển phần mềm giao diện người dùng bền vững. Tuy nhiên, trong nhiều năm, các nhà phát triển đã viết các bài kiểm thử đơn vị dễ hỏng, khẳng định các chi tiết triển khai, chẳng hạn như kiểm tra các biến trạng thái nội bộ của component hoặc kiểm thử các phương thức lớp riêng tư. React Testing Library đã cách mạng hóa việc kiểm thử giao diện người dùng bằng cách khuyến khích các nhà phát triển kiểm thử component từ góc độ người dùng cuối: tương tác với các phần tử DOM hiển thị thay vì kiểm tra trạng thái nội bộ của component.
Bộ Kiểm Thử & QA Giao Diện Người Dùng Hiện Đại
Trong hướng dẫn kỹ thuật này, chúng ta sẽ xem xét các thực hành tốt nhất cho React Testing Library sử dụng @testing-library/user-event v14+ trong các trình chạy kiểm thử hiện đại như Vitest và Jest. Nếu bạn đang quản lý các codebase lớn, việc kết hợp các mẫu này với hướng dẫn hiệu suất kiểm thử đơn vị Vitest Monorepo của chúng tôi sẽ giúp bộ kiểm thử của bạn chạy nhanh. Để xác thực trình duyệt đầy đủ, việc kết hợp kiểm thử đơn vị với kiểm thử E2E Playwright và kiểm thử hồi quy trực quan Playwright đảm bảo không có sự không ổn định nào trong toàn bộ quy trình CI/CD của bạn. Bạn sẽ tìm hiểu lý do tại sao user-event thay thế các phương thức fireEvent cũ, cách cấu trúc các khẳng định bất đồng bộ bằng cách sử dụng các truy vấn findBy, cách áp dụng các ưu tiên truy vấn có thể truy cập và cách mock các yêu cầu mạng backend một cách sạch sẽ bằng Mock Service Worker (MSW).
user-event Khác fireEvent Như Thế Nào Trong Việc Mô Phỏng Tương Tác Người Dùng Thực Tế?
API user-event mô phỏng các chuỗi sự kiện trình duyệt đầy đủ thực tế, bao gồm các sự kiện focus, hover, keypress và input, trong khi fireEvent gửi các sự kiện DOM tổng hợp riêng lẻ. Khi người dùng nhấp vào một nút trong trình duyệt web thực, trình duyệt sẽ kích hoạt một chuỗi sự kiện: pointerover, pointerdown, mousedown, focus, pointerup, mouseup và cuối cùng là click. Sử dụng fireEvent.click() chỉ gửi một sự kiện click tổng hợp duy nhất, bỏ qua các kiểm tra hover, focus và input validation.

Ngược lại, userEvent.click() kích hoạt toàn bộ quy trình sự kiện của trình duyệt. Nếu một nút bị vô hiệu hóa, userEvent.click() sẽ tôn trọng thuộc tính disabled và từ chối kích hoạt trình xử lý nhấp chuột, phản ánh hành vi trình duyệt thực. Sử dụng fireEvent.click() trên một nút bị vô hiệu hóa sẽ kích hoạt callback nhấp chuột một cách giả tạo, dẫn đến kết quả kiểm thử dương tính giả.
Hãy cùng xem xét sự khác biệt về kiến trúc giữa hai API:
+------------------------------------+------------------------------------+------------------------------------+
| Interaction Aspect | fireEvent (Legacy API) | user-event (Modern v14 API) |
+------------------------------------+------------------------------------+------------------------------------+
| Event Dispatch Mechanism | Single synthetic DOM event | Full realistic browser event chain |
| Disabled Element Enforcement | Ignores disabled attributes | Respects disabled DOM element state|
| Form Typing Behavior | Overwrites value attribute instantly| Triggers keydown, keypress, input |
| Setup Requirement | Direct static function call | Requires userEvent.setup() instance|
| Async Execution Contract | Synchronous execution | Asynchronous Promise execution |
+------------------------------------+------------------------------------+------------------------------------+
Hãy so sánh các triển khai mã để kiểm thử một component input form bằng cả hai phương pháp:
Kiểm Thử Input Form Với fireEvent Cũ
// Anti-Pattern: Testing with fireEvent skips realistic browser events
import { render, screen } from '@testing-library/react';
import fireEvent from '@testing-library/user-event';
import { UserForm } from './UserForm';
test('submits form using fireEvent (unrealistic event simulation)', () => {
render(<UserForm />);
const input = screen.getByLabelText(/username/i);
const button = screen.getByRole('button', { name: /submit/i });
// Instantly overwrites input value without keydown or input events
fireEvent.change(input, { target: { value: 'john_doe' } });
fireEvent.click(button);
expect(screen.getByText(/welcome, john_doe/i)).toBeInTheDocument();
});
Kiểm Thử Input Form Với user-event v14 Hiện Đại
Bây giờ, hãy xem xét triển khai theo kiểu thông thường sử dụng userEvent.setup():
// Recommended Pattern: Testing with userEvent v14 simulates realistic typing
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserForm } from './UserForm';
test('submits form using userEvent (realistic browser event simulation)', async () => {
// Always call userEvent.setup() before rendering the component!
const user = userEvent.setup();
render(<UserForm />);
const input = screen.getByLabelText(/username/i);
const button = screen.getByRole('button', { name: /submit/i });
// Simulates realistic keystrokes, focus changes, and character events
await user.type(input, 'john_doe');
await user.click(button);
expect(await screen.findByText(/welcome, john_doe/i)).toBeInTheDocument();
});
Lưu ý rằng userEvent.setup() khởi tạo một đối tượng phiên. Bạn phải gọi userEvent.setup() trước khi gọi render(), cho phép user-event đính kèm các trình lắng nghe cấp tài liệu và theo dõi trạng thái trên các thao tác điều hướng bàn phím.
Ngoài ra, userEvent.setup() chấp nhận các tùy chọn cấu hình để tăng tốc bộ hẹn giờ giả hoặc mô phỏng các tùy chọn con trỏ:
const user = userEvent.setup({
advanceTimers: vi.advanceTimersByTime,
delay: null // Disables default typing delay during fast unit test execution
});
Cấu hình delay: null tăng tốc thực thi bộ kiểm thử trong môi trường CI trong khi vẫn duy trì hành vi gửi sự kiện trình duyệt thực tế.
Các Kỹ Sư Nên Cấu Trúc Kiểm Thử Bất Đồng Bộ Với Truy Vấn findBy Và Trợ Giúp waitFor Như Thế Nào?
Các kỹ sư cấu trúc kiểm thử bất đồng bộ bằng cách chờ các truy vấn findBy cho các phần tử xuất hiện trong DOM và sử dụng các trợ giúp waitFor cho các khẳng định về việc loại bỏ hoặc thuộc tính của phần tử. Kiểm thử bất đồng bộ là một nguồn thường xuyên gây ra các bộ kiểm thử không ổn định khi các nhà phát triển sử dụng bộ hẹn giờ cố định như setTimeout hoặc cố gắng truy vấn các phần tử trước khi các yêu cầu mạng nền hoàn tất.

React Testing Library cung cấp ba loại tiền tố truy vấn riêng biệt cho các khẳng định DOM:
-
Bộ chọn Truy vấn getBy: Các truy vấn getBy thực hiện các khẳng định đồng bộ đối với các phần tử DOM hiện tại. Chúng trả về các phần tử DOM khớp ngay lập tức hoặc ném lỗi nếu không có hoặc nhiều phần tử khớp. Sử dụng getBy khi các phần tử được đảm bảo tồn tại trên lần render component ban đầu.
-
Bộ chọn Truy vấn queryBy: Các truy vấn queryBy thực hiện các kiểm tra đồng bộ cho sự không tồn tại của phần tử. Chúng trả về các phần tử DOM khớp hoặc
nullnếu không có phần tử nào khớp. Sử dụng queryBy khi khẳng định rằng một phần tử KHÔNG có mặt trong DOM (expect(screen.queryByText('Error')).toBeNull()). -
Bộ chọn Truy vấn findBy: Các truy vấn findBy thực hiện các truy vấn thăm dò bất đồng bộ đối với các phần tử DOM trong tương lai. Chúng trả về một Promise sẽ giải quyết khi một phần tử khớp xuất hiện trong DOM trong cửa sổ thời gian chờ 1.000ms. Sử dụng findBy khi chờ các phần tử được tạo sau khi tìm nạp API bất đồng bộ hoặc chuyển đổi trạng thái.
Hãy cùng xem xét cách kiểm thử một component danh sách người dùng bất đồng bộ tìm nạp dữ liệu từ một máy chủ backend:
// components/UserList.tsx
import { useState, useEffect } from 'react';
export function UserList() {
const [users, setUsers] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('/api/users')
.then((res) => {
if (!res.ok) throw new Error('Network error');
return res.json();
})
.then((data) => {
setUsers(data.map((u: any) => u.name));
setIsLoading(false);
})
.catch((err) => {
setError(err.message);
setIsLoading(false);
});
}, []);
if (isLoading) {
return <div role="status">Loading user directory...</div>;
}
if (error) {
return <div role="alert">Failed to fetch user directory: {error}</div>;
}
return (
<ul>
{users.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
);
}
Đây là bộ kiểm thử bất đồng bộ sạch sẽ sử dụng các truy vấn findBy và waitForElementToBeRemoved:
// components/UserList.test.tsx
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';
import { UserList } from './UserList';
test('renders loading spinner and displays fetched user directory', async () => {
render(<UserList />);
// Assert loading indicator is visible synchronously
const loadingIndicator = screen.getByRole('status');
expect(loadingIndicator).toHaveTextContent(/loading user directory/i);
// Option A: Wait for loading element to disappear from DOM
await waitForElementToBeRemoved(() => screen.queryByRole('status'));
// Option B: Await findBy query for async element appearance
const firstUser = await screen.findByText('Alice Smith');
expect(firstUser).toBeInTheDocument();
});
Sử dụng các truy vấn findBy loại bỏ các độ trễ sleep() cố định trong bộ kiểm thử của bạn. Truy vấn thăm dò DOM cứ sau 50ms cho đến khi phần tử xuất hiện hoặc hết thời gian chờ, giúp thực thi kiểm thử nhanh chóng và đáng tin cậy.
Các Quy Tắc Ưu Tiên Cho Bộ Chọn Truy Vấn Có Thể Truy Cập Trong React Testing Library Là Gì?
Các quy tắc ưu tiên cho bộ chọn truy vấn có thể truy cập yêu cầu sử dụng getByRole và getByLabelText trước tiên để phản ánh cách người dùng và công nghệ hỗ trợ điều hướng các ứng dụng web. Khi viết kiểm thử, hãy tránh chọn các phần tử bằng tên lớp CSS, thẻ phần tử HTML hoặc ID kiểm thử nội bộ trừ khi không thể có các lựa chọn thay thế có thể truy cập. Kiểm thử thông qua các vai trò ARIA và nhãn hiển thị đảm bảo rằng ứng dụng của bạn vẫn có thể truy cập được đối với người đọc màn hình và người dùng bàn phím.

Hãy cùng xem xét thứ tự ưu tiên truy vấn chính thức của React Testing Library:
+-----------------------------------------------------------------------------------+
| React Testing Library Query Selector Priority Hierarchy |
+-----------------------------------------------------------------------------------+
| Priority 1: Queries Accessible to Everyone |
| - getByRole (e.g., getByRole('button', { name: /save/i })) |
| - getByLabelText (e.g., getByLabelText(/email address/i)) |
| - getByPlaceholderText (e.g., getByPlaceholderText(/search/i)) |
| - getByText (e.g., getByText(/submit form/i)) |
| - getByDisplayValue (e.g., getByDisplayValue('john_doe')) |
+-----------------------------------------------------------------------------------+
| Priority 2: Semantic HTML Queries |
| - getByAltText (e.g., getByAltText(/company logo/i)) |
| - getByTitle (e.g., getByTitle(/close dialog/i)) |
+-----------------------------------------------------------------------------------+
| Priority 3: Test IDs (Last Resort Only) |
| - getByTestId (e.g., getByTestId('custom-canvas-node')) |
+-----------------------------------------------------------------------------------+
Hãy xem cách áp dụng các quy tắc ưu tiên phát hiện lỗi truy cập trong quá trình phát triển:
// Bad accessible markup: Missing label association
export function BadForm() {
return (
<div>
<span>Username</span>
<input type="text" id="user-input" />
<button onClick={() => {}}>Submit</button>
</div>
);
}
// Good accessible markup: Explicit HTML label association
export function GoodForm() {
return (
<form>
<label htmlFor="username-field">Username</label>
<input type="text" id="username-field" />
<button type="submit">Submit Form</button>
</form>
);
}
Khi bạn cố gắng viết một kiểm thử cho BadForm bằng cách sử dụng screen.getByLabelText(/username/i), kiểm thử sẽ ném lỗi vì phần tử <span> không phải là một <label> HTML. Viết kiểm thử bằng cách sử dụng getByLabelText buộc các nhà phát triển phần mềm phải sửa các đánh dấu truy cập HTML trước khi mã đến sản xuất.
Nếu một phần tử không có vai trò ARIA ngầm định (chẳng hạn như một <div> chung), hãy thêm các thuộc tính role hoặc aria-label để làm cho phần tử có thể truy cập được đối với cả người dùng trình đọc màn hình và bộ chọn truy vấn kiểm thử. Bạn sẽ xây dựng một ứng dụng toàn diện hơn cho người dùng dựa vào trình đọc màn hình.
Bạn Mock Các Hành Động Máy Chủ Bất Đồng Bộ Và Các Phụ Thuộc Dịch Vụ Vi Mô Trong Kiểm Thử Như Thế Nào?
Bạn mock các hành động máy chủ bất đồng bộ và các phụ thuộc dịch vụ vi mô bằng cách sử dụng Mock Service Worker MSW để chặn các cuộc gọi lớp mạng tại ranh giới HTTP. Thay vì mock trực tiếp các hàm JavaScript nội bộ hoặc các triển khai fetch, Mock Service Worker thiết lập một quy trình Service Worker hoặc bộ chặn yêu cầu Node.js để bắt các yêu cầu mạng ở lớp mạng.
Mocking ở ranh giới mạng đảm bảo rằng mã ứng dụng của bạn thực thi quá trình tuần tự hóa tìm nạp HTTP thực tế, phân tích cú pháp tiêu đề và logic xử lý lỗi trong quá trình kiểm thử đơn vị. Bạn không phải stub các hook nội bộ của React khi sử dụng MSW.
Hãy cùng kiểm tra cách thiết lập trình xử lý MSW cho bộ kiểm thử ứng dụng React:
// mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
// Intercept GET requests to /api/users
http.get('/api/users', () => {
return HttpResponse.json([
{ id: '1', name: 'Alice Smith' },
{ id: '2', name: 'Bob Jones' }
]);
}),
// Intercept POST requests to /api/users
http.post('/api/users', async ({ request }) => {
const body = (await request.json()) as { name: string };
if (!body.name) {
return new HttpResponse('Name field is required.', { status: 400 });
}
return HttpResponse.json(
{ id: '3', name: body.name },
{ status: 201 }
);
})
];
Đây là tệp cấu hình máy chủ MSW cho môi trường kiểm thử Vitest hoặc Jest:
// mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
Cuối cùng, hãy cấu hình tệp thiết lập kiểm thử toàn cục để khởi động và dừng máy chủ MSW một cách sạch sẽ:
// setupTests.ts
import '@testing-library/jest-dom';
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from './mocks/server';
// Start server before running test suites
beforeAll(() => server.listen());
// Reset inline request handler overrides after each test
afterEach(() => server.resetHandlers());
// Clean up server after tests finish
afterAll(() => server.close());
Bây giờ bạn có thể viết các kiểm thử tích hợp để khẳng định hành vi UI trong các lỗi máy chủ bằng cách ghi đè các trình xử lý MSW nội tuyến bên trong các tệp kiểm thử riêng lẻ:
// components/UserForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { server } from '../mocks/server';
import { UserForm } from './UserForm';
test('displays server error banner when API submission fails', async () => {
// Override default MSW handler for this specific test case
server.use(
http.post('/api/users', () => {
return new HttpResponse('Internal Server Error', { status: 500 });
})
);
const user = userEvent.setup();
render(<UserForm />);
await user.type(screen.getByLabelText(/username/i), 'new_user');
await user.click(screen.getByRole('button', { name: /submit/i }));
// Assert error banner appears on 500 response
const alert = await screen.findByRole('alert');
expect(alert).toHaveTextContent(/internal server error/i);
});
Mocking các yêu cầu mạng bằng MSW giữ cho các kiểm thử component của bạn không bị tách rời khỏi các triển khai máy chủ backend trong khi vẫn đảm bảo phân tích cú pháp phản hồi HTTP thực tế. Bạn sẽ thấy rằng các bộ kiểm thử frontend thực thi trong vài giây trong khi vẫn duy trì sự tin cậy hoàn toàn vào độ tin cậy của ứng dụng.
Hãy cùng xem xét cách xử lý mocking truy vấn GraphQL bằng các trình xử lý MSW:
// mocks/graphqlHandlers.ts
import { graphql, HttpResponse } from 'msw';
export const graphqlHandlers = [
graphql.query('GetUserProfile', () => {
return HttpResponse.json({
data: {
user: {
id: 'usr_99',
name: 'Sarah Connor',
email: 'sarah@example.com'
}
}
});
})
];
Tích hợp MSW cho cả API REST và GraphQL cho phép các nhóm kỹ thuật duy trì các mock mạng thống nhất trên các bộ kiểm thử đơn vị, tích hợp và E2E. Bạn không phải sao chép các định nghĩa mock trên các trình chạy kiểm thử khác nhau khi sử dụng các trình xử lý MSW.
+------------------------------------+------------------------------------+------------------------------------+
| Testing Layer Aspect | Isolated Unit Component Test | MSW Network Integration Test |
+------------------------------------+------------------------------------+------------------------------------+
| Mocking Strategy | Function prop stubs & Jest mocks | Network-level MSW HTTP interception|
| Execution Speed | Extremely Fast (< 10 ms per test) | Very Fast (< 50 ms per test) |
| Refactoring Resilience | Low (Breaks on prop signature edit)| High (Resilient to internal refactor)|
| Confidence Level | Moderate (Tests components in isolation)| High (Tests full component lifecycle)|
+------------------------------------+------------------------------------+------------------------------------+
Bằng cách ưu tiên các kiểm thử tích hợp mạng MSW hơn các stub thuộc tính component nông, các nhóm phần mềm xây dựng các bộ kiểm thử bền vững tồn tại qua các chu kỳ tái cấu trúc mã mạnh mẽ. Chúng tôi đã xác minh rằng các trình xử lý mock MSW giảm đáng kể chi phí bảo trì kiểm thử trong suốt vòng đời sản phẩm kéo dài nhiều năm. Ngoài ra, việc chạy các máy chủ mock MSW bên trong các kiểm thử Playwright dựa trên trình duyệt đảm bảo hành vi mạng giống hệt nhau trên các kiểm thử đơn vị component cục bộ và các bộ hồi quy end-to-end. Đó là một chiến thắng lớn cho tốc độ kỹ thuật, và các nhà phát triển không lãng phí hàng giờ để chống lại các mock mạng không ổn định. Bạn sẽ thấy rằng các chu kỳ phát hành phần mềm trở nên dễ đoán, trơn tru và được xác minh kỹ lưỡng trên các triển khai web sản xuất.
Bạn Cũng Có Thể Thích
- Tăng Cường React Với Rust Và WebAssembly: Hướng Dẫn Toàn Diện
- Rust Cho Các Nhà Phát Triển Frontend: Hướng Dẫn Chuyển Đổi Thực Tế
- Intersection Observer vs getBoundingClientRect trong JavaScript: Phân Tích Chuyên Sâu Hiệu Suất
- Làm Chủ SVG Trong React 19: Hiệu Suất, currentColor Động & Tối Ưu Hóa Bundle
Câu Hỏi Thường Gặp
user-event gửi các chuỗi sự kiện trình duyệt đầy đủ, thực tế (chẳng hạn như hover, pointerdown, mousedown, focus, pointerup, mouseup và click) trong khi tôn trọng các thuộc tính DOM như disabled. Ngược lại, fireEvent gửi các sự kiện DOM tổng hợp riêng lẻ mà không kích hoạt các sự kiện đi kèm hoặc thực thi trạng thái phần tử, dẫn đến các kiểm thử dương tính giả.
userEvent.setup() khởi tạo clipboard, trạng thái bàn phím, vị trí con trỏ và các trình lắng nghe sự kiện. Gọi setup() trước render() đảm bảo phiên bản userEvent liên kết đúng cách với tài liệu đang hoạt động và duy trì trạng thái tuần tự thực tế trên nhiều hành động người dùng trong kiểm thử của bạn.
Sử dụng các truy vấn findBy (ví dụ: findByRole, findByText) bất cứ khi nào một phần tử xuất hiện bất đồng bộ sau một promise, lệnh gọi API hoặc bộ hẹn giờ. Các truy vấn findBy trả về một Promise và tự động thử lại với thời gian chờ mặc định 1000ms sử dụng waitFor bên dưới. Chỉ sử dụng getBy cho các phần tử được đảm bảo có mặt đồng bộ trên lần render ban đầu.
Sử dụng await user.tab() để mô phỏng việc nhấn phím Tab. user-event sẽ tính toán chuỗi tabIndex của DOM, di chuyển document.activeElement đến nút có thể focus tiếp theo và kích hoạt các sự kiện focus và blur thích hợp trên các input tương ứng.
Các Hướng Dẫn & Phân Tích Chuyên Sâu Liên Quan
- Hướng Dẫn Kiểm Thử Đơn Vị & Hiệu Suất Monorepo Vitest
- Lỗi Hydration Next.js: Sửa 418, 423 & Không Khớp Văn Bản
- So Sánh Quản Lý Trạng Thái Zustand vs Jotai
- Hướng Dẫn Tái Xác Thực Động Next.js App Router
- Playwright vs Cypress Hiệu Suất & Điểm Chuẩn Bộ Nhớ 2026
Kiểm Tra Kiến Thức
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Vitest Monorepo: Kiểm thử đơn vị & Tối ưu hiệu suất (2026)
Hướng dẫn thực tế để tối ưu hiệu suất Vitest trong các monorepo TypeScript lớn: thread pools, barrel file imports, isolation flags và smart caching.
Read more
Next.js 15 Server Actions vs Route Handlers: So sánh kiến trúc chuyên sâu
Nắm vững khi nào nên chọn Server Actions so với Route Handlers trong Next.js 15, đi sâu vào progressive enhancement, hành vi caching, giao thức RPC và các ranh giới bảo mật.
Read more
Lỗi Hydration trong Next.js: Cách sửa "Text Content Mismatch" & Error 418 (2026)
Hướng dẫn sửa triệt để lỗi Hydration failed (#418), Text content does not match server-rendered HTML, lỗi dark mode flash next-themes và localStorage trong Next.js.
Read more