Unit test for react component
Unit test for react component
📚 Các khái niệm chính của Jest & RTL
Jest
describe(name, fn)
: Gom nhóm các bài test có liên quan lại với nhau. Giống như một "bộ test" (test suite).test(name, fn)
hoặcit(name, fn)
: Định nghĩa một trường hợp kiểm thử cụ thể (test case).expect(value)
: Tạo một "khẳng định" (assertion). Bạn sẽ dùng nó cùng với các "matcher".Matchers: Các hàm để kiểm tra giá trị. Ví dụ:
.toBe(value)
: So sánh giá trị chính xác (cho kiểu nguyên thủy)..toEqual(value)
: So sánh giá trị của object hoặc array..toBeInTheDocument()
: Kiểm tra một element có tồn tại trong DOM không (từ@testing-library/jest-dom
)..toHaveBeenCalled()
: Kiểm tra một hàm mock đã được gọi hay chưa.
React Testing Library (RTL)
render()
: "Vẽ" (render) component của bạn vào một DOM ảo để kiểm thử.screen
: Một đối tượng cung cấp các phương thức để truy vấn (query) các element trên DOM ảo vừa được render. Ví dụ:screen.getByText('Submit')
.fireEvent
: Dùng để mô phỏng các sự kiện của người dùng nhưclick
,change
,submit
...
📝 Hướng dẫn từng bước viết Unit Test
Hãy cùng viết test cho một component Counter
đơn giản.
Bước 1: Tạo Component cần test
Tạo file src/components/Counter.js
:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Counter</h1>
<p>Current count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
export default Counter;
Bước 2: Tạo file Test
Theo quy ước, file test sẽ có tên là [TênComponent].test.js
hoặc [TênComponent].spec.js
và đặt cùng cấp với file component.
Tạo file src/components/Counter.test.js
:
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; // Thêm các matcher hữu ích cho DOM
import Counter from './Counter';
// Bắt đầu một bộ test cho component Counter
describe('<Counter />', () => {
// Test case 1: Kiểm tra component render đúng trạng thái ban đầu
test('renders the initial state correctly', () => {
// 1. Render component
render(<Counter />);
// 2. Tìm các element quan trọng bằng các truy vấn của 'screen'
const titleElement = screen.getByText(/counter/i); // Tìm text chứa "counter", không phân biệt hoa thường
const countElement = screen.getByText(/current count: 0/i);
const incrementButton = screen.getByRole('button', { name: /increment/i }); // Tìm button có tên "Increment"
// 3. Khẳng định (Assert) rằng các element này tồn tại trong DOM
expect(titleElement).toBeInTheDocument();
expect(countElement).toBeInTheDocument();
expect(incrementButton).toBeInTheDocument();
});
// Test case 2: Kiểm tra hành vi khi người dùng nhấn nút "Increment"
test('increments the count when the increment button is clicked', () => {
// 1. Render component
render(<Counter />);
// 2. Tìm nút increment
const incrementButton = screen.getByRole('button', { name: /increment/i });
// 3. Mô phỏng hành động click của người dùng
fireEvent.click(incrementButton);
// 4. Tìm element hiển thị count và khẳng định rằng nó đã được cập nhật lên 1
const countElement = screen.getByText(/current count: 1/i);
expect(countElement).toBeInTheDocument();
});
// Test case 3: Kiểm tra hành vi khi người dùng nhấn nút "Decrement"
test('decrements the count when the decrement button is clicked', () => {
// 1. Render component
render(<Counter />);
// 2. Tìm nút decrement
const decrementButton = screen.getByRole('button', { name: /decrement/i });
// 3. Mô phỏng hành động click
fireEvent.click(decrementButton);
// 4. Khẳng định rằng count đã được cập nhật thành -1
const countElement = screen.getByText(/current count: -1/i);
expect(countElement).toBeInTheDocument();
});
});
Bước 3: Chạy Test
Mở terminal và chạy lệnh:
npm test
Jest sẽ tự động tìm tất cả các file có đuôi .test.js
hoặc .spec.js
và thực thi chúng. Bạn sẽ thấy kết quả báo cáo rằng cả 3 test case đều đã "PASS".
✨ Các kịch bản phổ biến khác
1. Test Props
Làm sao để test component nhận props? Rất đơn giản, chỉ cần truyền props vào khi render
.
Component Greeting.js
:
function Greeting({ name }) {
return <p>Hello, {name}!</p>;
}
Test Greeting.test.js
:
test('renders a greeting with the provided name', () => {
render(<Greeting name="Gemini" />);
const greetingElement = screen.getByText(/Hello, Gemini!/i);
expect(greetingElement).toBeInTheDocument();
});
2. Test sự kiện và Mock Function
Khi một button được click, nó sẽ gọi một hàm được truyền qua props. Ta có thể dùng jest.fn()
để tạo một "hàm giả" (mock function) và kiểm tra xem nó có được gọi hay không.
Component ButtonWrapper.js
:
function ButtonWrapper({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
Test ButtonWrapper.test.js
:
test('calls the onClick prop when clicked', () => {
const handleClick = jest.fn(); // Tạo một mock function
render(<ButtonWrapper onClick={handleClick}>Click Me</ButtonWrapper>);
const button = screen.getByText(/click me/i);
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1); // Khẳng định hàm đã được gọi 1 lần
});
3. Test Component bất đồng bộ (Async)
Khi component cần fetch dữ liệu từ API, ta cần test trạng thái "loading" và trạng thái "success".
Sử dụng các truy vấn
findBy*
của RTL (ví dụ:screen.findByText(...)
), chúng sẽ chờ một khoảng thời gian để element xuất hiện.Dùng
async/await
trong hàm test.
Test:
import { rest } from 'msw';
import { setupServer } from 'msw/node';
// Giả sử bạn dùng MSW (Mock Service Worker) để mock API
// ... (cấu hình server mock) ...
test('loads and displays user data', async () => {
render(<UserComponent />);
// Tìm và chờ element loading biến mất (nếu có)
// ...
// Chờ cho đến khi tên người dùng xuất hiện sau khi fetch API thành công
const userNameElement = await screen.findByText(/John Maverick/i);
expect(userNameElement).toBeInTheDocument();
});
👍 Mẹo và các phương pháp hay nhất (Best Practices)
Ưu tiên truy vấn theo vai trò (role): Dùng
getByRole
trước (ví dụ:getByRole('button')
), vì nó gần nhất với cách người dùng và công nghệ hỗ trợ (như trình đọc màn hình) tương tác với trang web.Sử dụng
getByText
cho nội dung văn bản: Đây là cách người dùng thực sự tìm kiếm thông tin.Hạn chế
getByTestId
: Chỉ dùngdata-testid
khi bạn không thể tìm thấy element bằng các cách khác. Lạm dụng nó sẽ khiến test của bạn xa rời trải nghiệm người dùng.Viết test độc lập: Mỗi test case (
it
hoặctest
) nên độc lập và không phụ thuộc vào kết quả của test case trước đó.Mỗi test chỉ nên kiểm tra một thứ: Giúp việc gỡ lỗi khi test thất bại trở nên dễ dàng hơn.
Chúc bạn viết test thành công! 🧪