Chào mừng các bạn quay trở lại với hành trình khám phá thế giới React cùng series “React Roadmap”! Sau khi đã đi qua các chủ đề quan trọng như cấu trúc component, quản lý state, xử lý sự kiện, và làm việc với Hooks, giờ là lúc chúng ta nói về một khía cạnh cực kỳ thiết yếu để xây dựng những ứng dụng mạnh mẽ và bền vững: Testing (kiểm thử). Cụ thể hơn, trong bài viết này, chúng ta sẽ tập trung vào Unit Test và làm quen với bộ đôi công cụ phổ biến nhất trong hệ sinh thái React: Jest và React Testing Library.
Có thể ban đầu, việc viết test có vẻ tốn thời gian hoặc phức tạp, nhưng tin tôi đi, đây là một khoản đầu tư cực kỳ xứng đáng. Unit test giúp bạn tự tin hơn rất nhiều khi refactor code, thêm tính năng mới, hay đơn giản là đảm bảo rằng những gì bạn viết ra đang hoạt động đúng như mong đợi. Đặc biệt với các bạn developer junior, việc học và áp dụng testing ngay từ đầu sẽ định hình tư duy code chất lượng và chuyên nghiệp.
Nếu bạn là người mới bắt đầu hoặc muốn xem lại lộ trình học React của chúng ta, hãy ghé thăm lại bài viết đầu tiên của series tại React Roadmap – Lộ trình học React 2025. Còn bây giờ, hãy cùng lặn sâu vào thế giới của Unit Test trong React nhé!
Mục lục
Unit Test Là Gì? Tại Sao Nó Quan Trọng Trong Phát Triển React?
Trong thế giới phát triển phần mềm, có nhiều loại kiểm thử khác nhau (Integration Test, End-to-End Test, Functional Test, v.v.). Unit Test là loại kiểm thử ở cấp độ nhỏ nhất. Mục tiêu của Unit Test là kiểm tra tính đúng đắn của một “đơn vị” code độc lập, thường là một hàm, một class, hoặc trong bối cảnh React, là một component nhỏ, riêng lẻ.
Một Unit Test điển hình sẽ:
- Nhận một đơn vị code làm đầu vào.
- Thực thi đơn vị code đó với các điều kiện cụ thể.
- Kiểm tra xem kết quả đầu ra có đúng với mong đợi hay không.
Vậy tại sao Unit Test lại quan trọng đối với các ứng dụng React?
- Bắt lỗi sớm: Unit test giúp phát hiện lỗi ngay trong quá trình phát triển, trước khi code được đưa lên môi trường tích hợp hoặc sản phẩm. Việc sửa lỗi ở giai đoạn này tiết kiệm thời gian và chi phí hơn rất nhiều soạt động.
- Tăng sự tự tin khi thay đổi code (Refactoring): Khi bạn cần cải tiến cấu trúc code (refactoring) hoặc thêm tính năng mới, bộ Unit Test hiện có sẽ hoạt động như một “lưới an toàn”. Nếu bạn vô tình làm hỏng một phần nào đó, các test sẽ thất bại ngay lập tức, cho phép bạn nhanh chóng xác định và sửa lỗi.
- Nâng cao chất lượng code và thiết kế: Việc suy nghĩ về cách viết test cho code sẽ khuyến khích bạn viết code module hơn, dễ test hơn (testable). Code testable thường là code có cấu trúc tốt, ít phụ thuộc và dễ hiểu.
- Tài liệu sống: Các bài test có thể hoạt động như một dạng tài liệu mô tả cách component hoặc hàm của bạn hoạt động trong các trường hợp khác nhau.
- Hỗ trợ làm việc nhóm: Khi làm việc trong một team, các test giúp đảm bảo rằng code của mọi người tích hợp với nhau một cách chính xác và không làm hỏng các phần đã hoạt động.
Đặc biệt với React, nơi chúng ta xây dựng giao diện từ các component nhỏ và kết hợp chúng lại (Kết hợp Component trong React: Tái sử dụng thật dễ dàng), việc kiểm tra từng component riêng lẻ là nền tảng để đảm bảo toàn bộ ứng dụng hoạt động trơn tru.
Bộ Đôi Hoàn Hảo: Jest và React Testing Library
Khi nói đến testing React, hai cái tên phổ biến nhất và thường đi cùng nhau là Jest và React Testing Library (RTL).
Jest – Người Chạy & Khung Kiểm Thử
Jest là một JavaScript Testing Framework được phát triển bởi Facebook (nay là Meta). Nó là một giải pháp kiểm thử “all-in-one”, cung cấp mọi thứ bạn cần để viết và chạy test:
- Test Runner: Jest tìm kiếm và chạy các file test của bạn.
- Assertion Library: Cung cấp các hàm
expect
và “matchers” (nhưtoBe
,toEqual
,toHaveBeenCalled
) để so sánh kết quả thực tế với kết quả mong đợi. - Mocking Library: Cho phép bạn “giả lập” (mock) các module hoặc hàm bên ngoài để cô lập đơn vị code cần test.
- Code Coverage Reports: Có khả năng tạo báo cáo về mức độ code của bạn đã được kiểm thử.
Jest được cấu hình mặc định trong hầu hết các dự án React hiện đại được tạo bằng Create React App hoặc Next.js, giúp việc bắt đầu trở nên rất dễ dàng.
React Testing Library (RTL) – Kiểm Thử Theo Cách Người Dùng
Trong khi Jest cung cấp nền tảng để chạy test và đưa ra các khẳng định (assertions), React Testing Library là thư viện chuyên biệt để kiểm thử React components. Triết lý cốt lõi của RTL là:
"The more your tests resemble the way your software is used, the more confidence they can give you."
Tạm dịch: “Bài test của bạn càng giống với cách phần mềm được sử dụng, chúng càng mang lại cho bạn nhiều sự tự tin.”
Điều này có nghĩa là RTL tập trung vào việc kiểm thử component bằng cách tương tác với nó giống như người dùng cuối sẽ làm. Thay vì kiểm tra chi tiết cấu trúc nội bộ của component (ví dụ: state, props, DOM nodes cụ thể được tạo ra bởi implemention detail), RTL khuyến khích bạn kiểm tra dựa trên những gì người dùng nhìn thấy và tương tác trên màn hình (ví dụ: văn bản hiển thị, các nút bấm, input fields, v.v.).
Sự kết hợp giữa Jest (làm nền tảng) và RTL (cách tiếp cận kiểm thử React components) là tiêu chuẩn vàng hiện nay trong cộng đồng React.
Cài Đặt và Cấu Hình (Nếu Chưa Có)
Như đã đề cập, nếu bạn sử dụng Create React App (create-react-app
) hoặc Next.js, Jest và React Testing Library thường đã được cài đặt và cấu hình sẵn. Bạn chỉ cần tạo các file test (thường có đuôi .test.js
, .spec.js
, .test.jsx
, hoặc .spec.jsx
) nằm trong thư mục chứa component hoặc trong thư mục __tests__
.
Nếu bạn đang xây dựng dự án từ đầu hoặc cần thêm vào dự án hiện có chưa có testing, bạn có thể cài đặt chúng bằng npm hoặc yarn:
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jest
Hoặc với yarn:
yarn add --dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jest
@testing-library/jest-dom
cung cấp các “custom matchers” hữu ích cho DOM, giúp các assertion trong test của bạn dễ đọc hơn (ví dụ: expect(element).toBeInTheDocument()
). @testing-library/user-event
giúp mô phỏng tương tác của người dùng chân thực hơn.
Các Khái Niệm Cơ Bản Trong Jest
Trước khi đi vào ví dụ cụ thể với React components, hãy nắm vững một vài khái niệm cơ bản của Jest.
describe
: Gom Nhóm Các Test
Hàm describe
được dùng để gom nhóm các bài test có liên quan. Nó giúp tổ chức code test của bạn một cách logic và dễ đọc. Thường bạn sẽ dùng describe
cho một component, một hook, hoặc một module cụ thể.
describe('Tên nhóm test (ví dụ: Component Button)', () => {
// Các test con sẽ nằm ở đây
});
test
(hoặc it
): Viết Một Bài Test Cụ Thể
Hàm test
(hoặc tên alias của nó là it
, cả hai đều hoạt động như nhau) được dùng để viết một bài test riêng lẻ. Chuỗi mô tả bài test nên rõ ràng và cho biết bài test đó đang kiểm tra cái gì (ví dụ: “nên hiển thị văn bản chào mừng”).
describe('Component Button', () => {
test('nên hiển thị văn bản được truyền qua props', () => {
// Logic kiểm thử
});
it('nên gọi hàm onClick khi được click', () => {
// Logic kiểm thử
});
});
expect
và Matchers: Khẳng Định Kết Quả
Đây là phần “kiểm tra” trong bài test. Hàm expect
nhận một giá trị và bạn dùng một “matcher” (phương thức) để so sánh giá trị đó với một kết quả mong đợi.
expect(2 + 2).toBe(4); // Sử dụng matcher toBe để so sánh giá trị primitive
expect([1, 2, 3]).toEqual([1, 2, 3]); // Sử dụng toEqual để so sánh giá trị của objects/arrays
expect(true).toBe(true);
expect('hello').not.toBe('bye'); // Sử dụng .not để phủ định assertion
expect(element).toBeInTheDocument(); // Matcher từ @testing-library/jest-dom
expect(mockFunc).toHaveBeenCalledTimes(1); // Matcher để kiểm tra hàm mock được gọi bao nhiêu lần
Có rất nhiều matchers trong Jest và @testing-library/jest-dom
. Bạn có thể tìm hiểu thêm trong tài liệu của họ.
Kiểm Thử Một Component Đơn Giản
Hãy bắt đầu với một component React đơn giản.
// src/components/WelcomeMessage.jsx
import React from 'react';
function WelcomeMessage({ name = 'Guest' }) {
return <h1>Chào mừng, {name}!</h1>;
}
export default WelcomeMessage;
Đây là một component functional đơn giản nhận một prop name
và hiển thị lời chào. Nếu không có name
, nó sẽ hiển thị “Guest”.
Bây giờ, chúng ta sẽ viết test cho component này sử dụng Jest và React Testing Library.
// src/components/WelcomeMessage.test.jsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import WelcomeMessage from './WelcomeMessage';
import '@testing-library/jest-dom'; // Nhập để có các matcher như toBeInTheDocument
describe('WelcomeMessage Component', () => {
test('nên hiển thị văn bản chào mừng mặc định khi không có prop name', () => {
render(<WelcomeMessage />); // Render component
// Sử dụng screen.getByText để tìm phần tử chứa văn bản 'Chào mừng, Guest!'
// Query này sẽ tìm kiếm trong toàn bộ DOM được render bởi React Testing Library
const linkElement = screen.getByText(/Chào mừng, Guest!/i); // /.../i là RegExp không phân biệt chữ hoa/thường
// Khẳng định rằng phần tử tìm được tồn tại trong DOM
expect(linkElement).toBeInTheDocument();
});
test('nên hiển thị văn bản chào mừng với tên được truyền qua prop', () => {
const testName = 'Developer';
render(<WelcomeMessage name={testName} />); // Render component với prop
// Tìm phần tử chứa văn bản 'Chào mừng, Developer!'
const linkElement = screen.getByText(`Chào mừng, ${testName}!`); // Sử dụng template literal
// Khẳng định rằng phần tử tồn tại
expect(linkElement).toBeInTheDocument();
});
});
Giải thích:
- Chúng ta import các hàm cần thiết từ
@testing-library/react
(render
vàscreen
), componentWelcomeMessage
, và các matcher từ@testing-library/jest-dom
. describe('WelcomeMessage Component', ...)
: Gom nhóm các test choWelcomeMessage
.test('...', ...)
: Định nghĩa một bài test cụ thể.render(<WelcomeMessage />)
hoặcrender(<WelcomeMessage name={testName} />)
: Hàm này từ RTL sẽ render component của bạn vào một môi trường DOM ảo trong Node.js.screen
: Là một đối tượng chứa các query để tìm kiếm các phần tử DOM đã được render. RTL cung cấp nhiều loại query khác nhau (getBy...
,queryBy...
,findBy...
).getByText
tìm kiếm phần tử dựa trên nội dung văn bản của nó.expect(...).toBeInTheDocument()
: Matcher này (từ@testing-library/jest-dom
) kiểm tra xem phần tử được tìm thấy có tồn tại trong cây DOM hay không.
Để chạy test, bạn mở terminal trong thư mục gốc dự án và gõ:
npm test
Hoặc:
yarn test
Jest sẽ tìm và chạy các file test, sau đó hiển thị kết quả.
Làm Việc Với Queries Trong React Testing Library
Việc tìm kiếm phần tử là bước quan trọng để kiểm tra nội dung hoặc tương tác. RTL cung cấp nhiều loại query, mỗi loại có mức độ ưu tiên và mục đích sử dụng khác nhau.
Nguyên tắc vàng khi chọn query là: Hãy sử dụng query mà người dùng sẽ sử dụng để định vị phần tử đó! Điều này giúp test của bạn bền vững hơn khi cấu trúc DOM bên trong component thay đổi.
Các loại query phổ biến (được liệt kê theo thứ tự ưu tiên khuyến nghị):
getByRole
: Tìm kiếm theo vai trò (role) của phần tử trên Accessibility Tree (cây hỗ trợ người khuyết tật), ví dụ: button, checkbox, link, heading, textbox. Đây là query ưu tiên nhất vì nó phản ánh cách người dùng sử dụng các công nghệ hỗ trợ (như screen readers) để tương tác.getByLabelText
: Tìm kiếm theo label của một form control (input, textarea, select). Rất quan trọng cho các form.getByPlaceholderText
: Tìm kiếm theo placeholder của input/textarea.getByText
: Tìm kiếm theo nội dung văn bản của phần tử. Hữu ích cho các đoạn văn, tiêu đề, hoặc nút bấm có văn bản.getByDisplayValue
: Tìm kiếm theo giá trị hiện tại của input, textarea, hoặc select.getByAltText
: Tìm kiếm theo văn bản alternative text của ảnh (<img alt="...">
) hoặc area elements. Quan trọng cho khả năng tiếp cận (accessibility).getByTitle
: Tìm kiếm theo thuộc tínhtitle
. Không được khuyến khích nhiều vì thuộc tính title không phải lúc nào cũng được công nghệ hỗ trợ đọc.getByTestId
: Tìm kiếm theo thuộc tínhdata-testid
tùy chỉnh. Chỉ nên dùng khi không có cách nào khác để định vị phần tử một cách “người dùng”.
Ngoài các query getBy*
(sẽ throw lỗi nếu không tìm thấy phần tử), còn có:
queryBy*
: Trả vềnull
nếu không tìm thấy. Hữu ích khi bạn muốn kiểm tra xem một phần tử KHÔNG tồn tại trên trang.findBy*
: Trả về một Promise. Hữu ích khi kiểm thử các phần tử xuất hiện bất đồng bộ (sau khi fetch data, animation kết thúc, v.v.). RTL sử dụngwaitFor
ngầm bên trong.
Mỗi loại query cũng có phiên bản getAllBy*
, queryAllBy*
, findAllBy*
để tìm kiếm nhiều phần tử cùng lúc.
Đây là bảng tóm tắt các query phổ biến:
Loại Query | Mục Đích | Cách Sử Dụng Phổ Biến | Ưu Tiên | Throw Error / Trả về null / Promise |
---|---|---|---|---|
getByRole |
Theo vai trò Accessibility | screen.getByRole('button', { name: 'Click me' }) |
Cao nhất | Throw Error nếu không tìm thấy 1 phần tử |
getByLabelText |
Theo label của form control | screen.getByLabelText('Username') |
Cao | Throw Error nếu không tìm thấy 1 phần tử |
getByText |
Theo nội dung văn bản | screen.getByText('Submit') |
Trung bình | Throw Error nếu không tìm thấy 1 phần tử |
getByTestId |
Theo thuộc tính data-testid |
screen.getByTestId('submit-button') |
Thấp (chỉ dùng khi không còn cách nào) | Throw Error nếu không tìm thấy 1 phần tử |
queryBy... |
Kiểm tra sự vắng mặt | screen.queryByText('Loading...') |
Trả về null nếu không tìm thấy |
|
findBy... |
Tìm bất đồng bộ | await screen.findByText('Data loaded') |
Trả về Promise, Throw Error nếu không tìm thấy sau timeout |
Kiểm Thử Tương Tác Người Dùng (Events)
Ứng dụng React thường có tương tác: click nút, nhập liệu vào input, v.v. React Testing Library, kết hợp với @testing-library/user-event
, giúp mô phỏng các tương tác này.
@testing-library/user-event
là một thư viện mô phỏng các sự kiện của người dùng một cách chân thực hơn so với việc trực tiếp gọi fireEvent
của RTL. Ví dụ, khi bạn mô phỏng việc nhập liệu bằng userEvent.type
, nó sẽ kích hoạt các sự kiện keyDown
, keyPress
, input
, keyUp
giống như người dùng thật gõ phím.
Hãy xem một ví dụ với component Button có khả năng gọi hàm onClick
.
Component Button:
// src/components/Button.jsx
import React from 'react';
function Button({ onClick, children }) {
return (
<button type="button" onClick={onClick}>
{children}
</button>
);
}
export default Button;
Test cho component Button:
// src/components/Button.test.jsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; // Import userEvent
import Button from './Button';
import '@testing-library/jest-dom';
describe('Button Component', () => {
test('nên hiển thị văn bản được truyền qua children', () => {
render(<Button>Click Me</Button>);
const buttonElement = screen.getByRole('button', { name: 'Click Me' });
expect(buttonElement).toBeInTheDocument();
});
test('nên gọi hàm onClick khi nút được click', async () => {
const mockOnClick = jest.fn(); // Tạo một hàm mock
render(<Button onClick={mockOnClick}>Clickable Button</Button>);
// Tìm nút bằng Role và Name
const buttonElement = screen.getByRole('button', { name: 'Clickable Button' });
// Mô phỏng hành động click của người dùng
await userEvent.click(buttonElement); // user-event là bất đồng bộ, nên dùng await
// Khẳng định rằng hàm mock đã được gọi
expect(mockOnClick).toHaveBeenCalledTimes(1); // Kiểm tra hàm được gọi 1 lần
expect(mockOnClick).toHaveBeenCalled(); // Hoặc chỉ kiểm tra hàm có được gọi hay không
});
});
Giải thích:
- Chúng ta import
userEvent
. - Sử dụng
jest.fn()
để tạo ra một “hàm mock” (mock function). Hàm mock là một hàm giả lập cho phép chúng ta theo dõi xem nó có được gọi không, được gọi bao nhiêu lần, với những đối số nào, v.v. Đây là kỹ thuật quan trọng để kiểm tra xem một sự kiện có kích hoạt hàm xử lý đúng hay không. - Truyền hàm mock này vào prop
onClick
của component Button. - Sử dụng
userEvent.click(buttonElement)
để mô phỏng hành động click chuột lên nút. VìuserEvent
thực hiện nhiều hành động nhỏ mô phỏng người dùng thật, nó trả về một Promise, nên chúng ta cần sử dụngasync/await
. - Cuối cùng, sử dụng các matcher của Jest dành cho hàm mock (
toHaveBeenCalledTimes
,toHaveBeenCalled
) để kiểm tra xemmockOnClick
có được gọi như mong đợi hay không.
Tương tự, bạn có thể mô phỏng các tương tác khác như nhập liệu vào input:
test('nên cập nhật giá trị input khi người dùng nhập liệu', async () => {
render(<input type="text" placeholder="Enter text" />);
const inputElement = screen.getByPlaceholderText('Enter text');
await userEvent.type(inputElement, 'Hello World');
expect(inputElement).toHaveValue('Hello World'); //toHaveValue là matcher từ @testing-library/jest-dom
});
Khi nói về xử lý sự kiện trong React, chúng ta đã tìm hiểu về cách các sự kiện hoạt động (Xử Lý Sự Kiện trong React: Cách Tiếp Cận ‘React Way’). Unit test giúp bạn đảm bảo rằng các hàm xử lý sự kiện này được gọi đúng lúc và hoạt động chính xác dựa trên tương tác của người dùng.
Kiểm Thử Components Sử Dụng Hooks
Trong kỷ nguyên của React Hooks (useState và useEffect: Siêu Năng Lực Nhập Môn của React), hầu hết các component của chúng ta đều sử dụng useState
, useEffect
, useContext
(Sử dụng useContext để Quản lý Global State), v.v. React Testing Library khuyến khích bạn không nên test trực tiếp các hook một cách riêng lẻ (isolation), mà nên test component sử dụng hook đó.
Ví dụ, nếu bạn có một component sử dụng useState
để đếm số lần click:
// src/components/Counter.jsx
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increase</button>
</div>
);
}
export default Counter;
Thay vì test riêng hàm useState
hoặc biến count
, chúng ta sẽ test thông qua tương tác với component:
// src/components/Counter.test.jsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
import '@testing-library/jest-dom';
describe('Counter Component', () => {
test('nên tăng số đếm khi nút được click', async () => {
render(<Counter />);
// Tìm phần tử hiển thị số đếm ban đầu
const countElement = screen.getByText(/Count: 0/i);
expect(countElement).toBeInTheDocument();
// Tìm nút tăng
const buttonElement = screen.getByRole('button', { name: 'Increase' });
// Mô phỏng 2 lần click
await userEvent.click(buttonElement);
await userEvent.click(buttonElement);
// Tìm phần tử hiển thị số đếm sau khi click và khẳng định giá trị
const updatedCountElement = screen.getByText(/Count: 2/i);
expect(updatedCountElement).toBeInTheDocument();
// Đảm bảo phần tử hiển thị số đếm cũ không còn
expect(countElement).not.toBeInTheDocument(); // queryByText sẽ hữu ích ở đây nếu muốn check absence
});
});
Trong ví dụ này, chúng ta kiểm tra component Counter
bằng cách tương tác với nút “Increase” (hành động người dùng) và kiểm tra xem văn bản hiển thị số đếm có thay đổi đúng hay không (những gì người dùng nhìn thấy). Chúng ta không cần quan tâm component sử dụng useState
hay useReducer
(Quản lý State với useReducer), chỉ cần kiểm tra hành vi cuối cùng.
Đối với useEffect
, bạn có thể kiểm thử bằng cách render component, chờ cho hiệu ứng chạy xong (nếu nó bất đồng bộ, dùng findBy*
hoặc waitFor
), và kiểm tra xem DOM đã được cập nhật đúng chưa hoặc một hàm mock đã được gọi chưa.
Mocking Các Phụ Thuộc Bên Ngoài
Unit test nên kiểm tra các “đơn vị” code một cách độc lập. Điều này có nghĩa là chúng ta cần loại bỏ hoặc giả lập (mock) các phụ thuộc bên ngoài như API calls, localStorage, các module của bên thứ ba, v.v. Jest có khả năng mocking mạnh mẽ.
Ví dụ, nếu component của bạn gọi một API để lấy dữ liệu hiển thị:
// src/components/UserData.jsx
import React, { useEffect, useState } from 'react';
import { fetchUser } from '../api'; // Giả sử có module api.js
function UserData({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(data => setUser(data))
.catch(error => console.error(error))
.finally(() => setLoading(false));
}, [userId]); // Dependency array, xem lại useEffect nhé!
if (loading) {
return <div>Loading user data...</div>;
}
if (!user) {
return <div>User not found.</div>;
}
return (
<div>
<h2>User Details</h2>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
);
}
export default UserData;
Trong unit test cho UserData
, chúng ta không muốn thực sự gọi API. Thay vào đó, chúng ta sẽ mock module ../api
.
Test cho UserData:
// src/components/UserData.test.jsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import UserData from './UserData';
import { fetchUser } from '../api'; // Import hàm cần mock
import '@testing-library/jest-dom';
// Sử dụng jest.mock để mock toàn bộ module '../api'
jest.mock('../api');
// Ép kiểu fetchUser (đã bị mock) để dùng các matcher của hàm mock
const mockFetchUser = fetchUser as jest.Mock;
describe('UserData Component', () => {
// Reset mock trước mỗi test để đảm bảo độc lập
beforeEach(() => {
mockFetchUser.mockClear();
});
test('nên hiển thị trạng thái loading ban đầu', () => {
// Mock fetchUser để nó trả về Promise chưa được resolved ngay lập tức
mockFetchUser.mockReturnValue(new Promise(() => {})); // Promise không bao giờ resolve/reject
render(<UserData userId="123" />);
expect(screen.getByText('Loading user data...')).toBeInTheDocument();
});
test('nên hiển thị dữ liệu người dùng sau khi fetch thành công', async () => {
const mockUserData = { name: 'John Doe', email: 'john.doe@example.com' };
// Mock fetchUser để trả về dữ liệu mock
mockFetchUser.mockResolvedValue(mockUserData);
render(<UserData userId="123" />);
// Sử dụng findByText vì dữ liệu được load bất đồng bộ
// findByText sẽ chờ một khoảng thời gian cho đến khi phần tử xuất hiện
const userNameElement = await screen.findByText(/Name: John Doe/i);
const userEmailElement = await screen.findByText(/Email: john.doe@example.com/i);
expect(userNameElement).toBeInTheDocument();
expect(userEmailElement).toBeInTheDocument();
expect(screen.queryByText('Loading user data...')).not.toBeInTheDocument(); // Kiểm tra element loading đã biến mất
});
test('nên hiển thị thông báo khi người dùng không tồn tại', async () => {
// Mock fetchUser để trả về lỗi (hoặc resolve với null tùy logic api)
mockFetchUser.mockResolvedValue(null); // Hoặc mockRejectedValue(new Error('Not Found'))
render(<UserData userId="456" />);
const notFoundElement = await screen.findByText('User not found.');
expect(notFoundElement).toBeInTheDocument();
expect(screen.queryByText('Loading user data...')).not.toBeInTheDocument();
});
});
Giải thích:
jest.mock('../api');
: Dòng này báo cho Jest biết rằng chúng ta muốn mock toàn bộ module../api
. KhiUserData
importfetchUser
, nó sẽ nhận được một hàm mock thay vì hàm thật.const mockFetchUser = fetchUser as jest.Mock;
: Ép kiểu để TypeScript hiểufetchUser
là một hàm mock và cung cấp các phương thức nhưmockClear
,mockReturnValue
,mockResolvedValue
, v.v.beforeEach(() => { mockFetchUser.mockClear(); });
: Quan trọng! Trước mỗi bài test, chúng ta cần “làm sạch” trạng thái của hàm mock (ví dụ: xóa lịch sử các lần gọi) để đảm bảo các test không ảnh hưởng lẫn nhau.mockFetchUser.mockReturnValue(...)
,mockResolvedValue(...)
,mockRejectedValue(...)
: Các phương thức này cho phép chúng ta định nghĩa hành vi của hàm mock.mockResolvedValue
đặc biệt hữu ích khi mock các hàm bất đồng bộ trả về Promise thành công, cònmockRejectedValue
cho trường hợp lỗi.- Chúng ta sử dụng
await screen.findByText(...)
để chờ cho dữ liệu (đã được mock resolve) được hiển thị trên UI.
Việc mock giúp chúng ta kiểm soát môi trường test và chỉ tập trung vào logic của component đang xét, không bị ảnh hưởng bởi các yếu động bên ngoài (như kết nối mạng, trạng thái server API, …).
Khi làm việc với việc gọi API trong React, chúng ta đã tìm hiểu các cách như Fetch, Axios, hoặc các thư viện chuyên dụng như React Query/SWR (Cách Gọi API Với Axios và Fetch, React Query vs SWR). Kỹ thuật mocking này áp dụng cho bất kể bạn dùng thư viện nào để fetch data.
Best Practices Khi Viết Unit Test React
Để viết các bài test hiệu quả và dễ bảo trì, hãy lưu ý một số điểm sau:
- Test hành vi, không phải chi tiết implemention: Đây là triết lý cốt lõi của React Testing Library. Đừng kiểm tra state nội bộ của component (trừ khi cần thiết cho một số trường hợp đặc biệt), hay cấu trúc DOM cụ thể mà chỉ dựa vào class CSS hay id ngẫu nhiên. Hãy test dựa trên những gì người dùng nhìn thấy và tương tác.
- Sử dụng các query theo ưu tiên của RTL: Bắt đầu với
getByRole
, sau đó đến các query dựa trên label, text, v.v. Chỉ dùnggetByTestId
khi không còn lựa chọn “người dùng” nào khác. - Giữ test nhỏ và tập trung: Mỗi bài test (
test
block) nên kiểm tra một kịch bản hoặc một khía cạnh cụ thể của đơn vị code. - Viết mô tả test rõ ràng: Chuỗi mô tả trong
test(...)
nên dễ hiểu, cho biết bài test này đang kiểm tra điều gì. - Đảm bảo test độc lập: Mỗi bài test nên chạy độc lập với các bài test khác. Sử dụng
beforeEach
,afterEach
để setup/cleanup môi trường nếu cần (ví dụ: reset mock, làm sạch DOM). - Kiểm tra các trường hợp biên: Đừng chỉ test các trường hợp “happy path”. Hãy kiểm tra khi props thiếu, dữ liệu trả về rỗng, API lỗi, v.v.
- Cân bằng giữa coverage và giá trị: Code coverage (tỷ lệ phần trăm code được test) là một chỉ số hữu ích, nhưng đừng chạy theo coverage một cách mù quáng. Quan trọng hơn là viết các bài test có giá trị, kiểm tra các luồng quan trọng và các chức năng cốt lõi.
Kết Luận
Unit test là một phần không thể thiếu của quy trình phát triển phần mềm chuyên nghiệp, và đặc biệt quan trọng khi xây dựng ứng dụng React. Với sự hỗ trợ mạnh mẽ từ Jest và React Testing Library, việc bắt đầu viết test cho các component của bạn trở nên dễ dàng và hiệu quả hơn bao giờ hết.
Áp dụng Unit test không chỉ giúp bạn bắt lỗi sớm và tự tin hơn khi thay đổi code, mà còn góp phần nâng cao chất lượng và tính dễ bảo trì của toàn bộ ứng dụng. Hãy coi testing là một phần của quá trình code, chứ không phải là một nhiệm vụ riêng biệt sau khi hoàn thành chức năng.
Chúng ta đã cùng tìm hiểu về những khái niệm cơ bản nhất của Unit test trong React, cách sử dụng Jest và React Testing Library để render component, tìm kiếm phần tử, mô phỏng tương tác người dùng và mock các phụ thuộc. Đây là nền tảng vững chắc để bạn tiếp tục khám phá sâu hơn về các loại kiểm thử khác và áp dụng testing vào các dự án thực tế.
Hy vọng bài viết này đã cung cấp cho bạn cái nhìn rõ ràng và động lực để bắt đầu viết Unit test cho các ứng dụng React của mình. Đừng ngần ngại thử nghiệm và áp dụng những kiến thức đã học!
Hẹn gặp lại các bạn trong những bài viết tiếp theo của series “React Roadmap”, nơi chúng ta sẽ tiếp tục khám phá những khía cạnh khác của việc xây dựng ứng dụng React hiện đại!