Làm chủ React Testing Library (RTL) với hướng dẫn đầy đủ này. Học cách viết các bài test hiệu quả, dễ bảo trì và tập trung vào người dùng cho ứng dụng React của bạn, tập trung vào các phương pháp hay nhất và ví dụ thực tế.
React Testing Library: Hướng dẫn Toàn diện
Trong bối cảnh phát triển web có nhịp độ nhanh ngày nay, việc đảm bảo chất lượng và độ tin cậy của các ứng dụng React của bạn là vô cùng quan trọng. React Testing Library (RTL) đã nổi lên như một giải pháp phổ biến và hiệu quả để viết các bài test tập trung vào góc nhìn của người dùng. Hướng dẫn này cung cấp một cái nhìn tổng quan hoàn chỉnh về RTL, bao gồm mọi thứ từ các khái niệm cơ bản đến các kỹ thuật nâng cao, giúp bạn xây dựng các ứng dụng React mạnh mẽ và dễ bảo trì.
Tại sao nên chọn React Testing Library?
Các phương pháp kiểm thử truyền thống thường dựa vào chi tiết triển khai, làm cho các bài test trở nên mong manh và dễ bị hỏng khi có những thay đổi nhỏ về mã nguồn. Ngược lại, RTL khuyến khích bạn kiểm thử các component của mình như cách người dùng sẽ tương tác với chúng, tập trung vào những gì người dùng nhìn thấy và trải nghiệm. Cách tiếp cận này mang lại một số lợi thế chính:
- Kiểm thử hướng người dùng: RTL thúc đẩy việc viết các bài test phản ánh góc nhìn của người dùng, đảm bảo rằng ứng dụng của bạn hoạt động như mong đợi từ quan điểm của người dùng cuối.
- Giảm sự mong manh của Test: Bằng cách tránh kiểm thử các chi tiết triển khai, các bài test của RTL ít có khả năng bị hỏng khi bạn tái cấu trúc mã nguồn, dẫn đến các bài test dễ bảo trì và mạnh mẽ hơn.
- Cải thiện Thiết kế Mã nguồn: RTL khuyến khích bạn viết các component dễ tiếp cận và dễ sử dụng, dẫn đến thiết kế mã nguồn tổng thể tốt hơn.
- Tập trung vào Khả năng Tiếp cận: RTL giúp việc kiểm thử khả năng tiếp cận của các component của bạn trở nên dễ dàng hơn, đảm bảo rằng ứng dụng của bạn có thể sử dụng được bởi mọi người.
- Quy trình Kiểm thử được Đơn giản hóa: RTL cung cấp một API đơn giản và trực quan, giúp việc viết và duy trì các bài test trở nên dễ dàng hơn.
Thiết lập Môi trường Kiểm thử của bạn
Trước khi có thể bắt đầu sử dụng RTL, bạn cần thiết lập môi trường kiểm thử của mình. Điều này thường bao gồm việc cài đặt các dependency cần thiết và cấu hình framework kiểm thử của bạn.
Điều kiện tiên quyết
- Node.js và npm (hoặc yarn): Đảm bảo rằng bạn đã cài đặt Node.js và npm (hoặc yarn) trên hệ thống của mình. Bạn có thể tải chúng từ trang web chính thức của Node.js.
- Dự án React: Bạn nên có một dự án React hiện có hoặc tạo một dự án mới bằng Create React App hoặc một công cụ tương tự.
Cài đặt
Cài đặt các gói sau bằng npm hoặc yarn:
npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Hoặc, sử dụng yarn:
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Giải thích các gói:
- @testing-library/react: Thư viện cốt lõi để kiểm thử các component React.
- @testing-library/jest-dom: Cung cấp các matcher Jest tùy chỉnh để xác nhận về các nút DOM.
- Jest: Một framework kiểm thử JavaScript phổ biến.
- babel-jest: Một transformer của Jest sử dụng Babel để biên dịch mã nguồn của bạn.
- @babel/preset-env: Một preset của Babel xác định các plugin và preset Babel cần thiết để hỗ trợ các môi trường mục tiêu của bạn.
- @babel/preset-react: Một preset của Babel cho React.
Cấu hình
Tạo một tệp `babel.config.js` ở thư mục gốc của dự án với nội dung sau:
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
Cập nhật tệp `package.json` của bạn để bao gồm một script test:
{
"scripts": {
"test": "jest"
}
}
Tạo một tệp `jest.config.js` ở thư mục gốc của dự án để cấu hình Jest. Một cấu hình tối thiểu có thể trông như thế này:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/src/setupTests.js'],
};
Tạo một tệp `src/setupTests.js` với nội dung sau. Điều này đảm bảo rằng các matcher Jest DOM có sẵn trong tất cả các bài test của bạn:
import '@testing-library/jest-dom/extend-expect';
Viết bài Test đầu tiên của bạn
Hãy bắt đầu với một ví dụ đơn giản. Giả sử bạn có một component React hiển thị một thông điệp chào mừng:
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default Greeting;
Bây giờ, hãy viết một bài test cho component này:
// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('renders a greeting message', () => {
render(<Greeting name="World" />);
const greetingElement = screen.getByText(/Hello, World!/i);
expect(greetingElement).toBeInTheDocument();
});
Giải thích:
- `render`: Hàm này render component vào DOM.
- `screen`: Đối tượng này cung cấp các phương thức để truy vấn DOM.
- `getByText`: Phương thức này tìm một phần tử theo nội dung văn bản của nó. Cờ `/i` làm cho việc tìm kiếm không phân biệt chữ hoa chữ thường.
- `expect`: Hàm này được sử dụng để đưa ra các xác nhận về hành vi của component.
- `toBeInTheDocument`: Matcher này xác nhận rằng phần tử có mặt trong DOM.
Để chạy bài test, thực thi lệnh sau trong terminal của bạn:
npm test
Nếu mọi thứ được cấu hình chính xác, bài test sẽ thành công.
Các Truy vấn RTL Phổ biến
RTL cung cấp nhiều phương thức truy vấn khác nhau để tìm các phần tử trong DOM. Các truy vấn này được thiết kế để bắt chước cách người dùng tương tác với ứng dụng của bạn.
`getByRole`
Truy vấn này tìm một phần tử theo vai trò ARIA của nó. Đây là một phương pháp hay để sử dụng `getByRole` bất cứ khi nào có thể, vì nó thúc đẩy khả năng tiếp cận và đảm bảo rằng các bài test của bạn có khả năng phục hồi trước những thay đổi trong cấu trúc DOM cơ bản.
<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
Truy vấn này tìm một phần tử theo văn bản của nhãn liên quan đến nó. Nó hữu ích cho việc kiểm thử các phần tử biểu mẫu.
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
Truy vấn này tìm một phần tử theo văn bản giữ chỗ của nó.
<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
Truy vấn này tìm một phần tử hình ảnh theo văn bản thay thế (alt text) của nó. Điều quan trọng là phải cung cấp văn bản thay thế có ý nghĩa cho tất cả các hình ảnh để đảm bảo khả năng tiếp cận.
<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
Truy vấn này tìm một phần tử theo thuộc tính title của nó.
<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
Truy vấn này tìm một phần tử theo giá trị hiển thị của nó. Điều này hữu ích cho việc kiểm thử các ô nhập liệu của biểu mẫu với các giá trị được điền sẵn.
<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();
Truy vấn `getAllBy*`
Ngoài các truy vấn `getBy*`, RTL cũng cung cấp các truy vấn `getAllBy*`, trả về một mảng các phần tử khớp. Chúng hữu ích khi bạn cần xác nhận rằng nhiều phần tử có cùng đặc điểm có mặt trong DOM.
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
Truy vấn `queryBy*`
Các truy vấn `queryBy*` tương tự như các truy vấn `getBy*`, nhưng chúng trả về `null` nếu không tìm thấy phần tử nào khớp, thay vì ném ra lỗi. Điều này hữu ích khi bạn muốn xác nhận rằng một phần tử *không* có mặt trong DOM.
const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();
Truy vấn `findBy*`
Các truy vấn `findBy*` là các phiên bản bất đồng bộ của các truy vấn `getBy*`. Chúng trả về một Promise sẽ được giải quyết khi tìm thấy phần tử khớp. Chúng hữu ích cho việc kiểm thử các hoạt động bất đồng bộ, chẳng hạn như tìm nạp dữ liệu từ API.
// Mô phỏng việc tìm nạp dữ liệu bất đồng bộ
const fetchData = () => new Promise(resolve => {
setTimeout(() => resolve('Data Loaded!'), 1000);
});
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
test('loads data asynchronously', async () => {
render(<MyComponent />);
const dataElement = await screen.findByText('Data Loaded!');
expect(dataElement).toBeInTheDocument();
});
Mô phỏng Tương tác Người dùng
RTL cung cấp các API `fireEvent` và `userEvent` để mô phỏng các tương tác của người dùng, chẳng hạn như nhấp vào nút, nhập vào các trường nhập liệu và gửi biểu mẫu.
`fireEvent`
`fireEvent` cho phép bạn kích hoạt các sự kiện DOM một cách lập trình. Đây là một API cấp thấp hơn cho phép bạn kiểm soát chi tiết các sự kiện được kích hoạt.
<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';
test('simulates a button click', () => {
const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
render(<button onClick={() => alert('Button clicked!')}>Click me</button>);
const buttonElement = screen.getByRole('button');
fireEvent.click(buttonElement);
expect(alertMock).toHaveBeenCalledTimes(1);
alertMock.mockRestore();
});
`userEvent`
`userEvent` là một API cấp cao hơn mô phỏng các tương tác của người dùng một cách thực tế hơn. Nó xử lý các chi tiết như quản lý tiêu điểm và thứ tự sự kiện, làm cho các bài test của bạn mạnh mẽ hơn và ít bị mong manh hơn.
<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';
test('simulates typing in an input field', () => {
const inputElement = screen.getByRole('textbox');
userEvent.type(inputElement, 'Hello, world!');
expect(inputElement).toHaveValue('Hello, world!');
});
Kiểm thử Mã Bất đồng bộ
Nhiều ứng dụng React liên quan đến các hoạt động bất đồng bộ, chẳng hạn như tìm nạp dữ liệu từ một API. RTL cung cấp một số công cụ để kiểm thử mã bất đồng bộ.
`waitFor`
`waitFor` cho phép bạn chờ một điều kiện trở thành sự thật trước khi đưa ra một xác nhận. Nó hữu ích cho việc kiểm thử các hoạt động bất đồng bộ mất một khoảng thời gian để hoàn thành.
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
setTimeout(() => {
setData('Data loaded!');
}, 1000);
}, []);
return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';
test('waits for data to load', async () => {
render(<MyComponent />);
await waitFor(() => screen.getByText('Data loaded!'));
const dataElement = screen.getByText('Data loaded!');
expect(dataElement).toBeInTheDocument();
});
Truy vấn `findBy*`
Như đã đề cập trước đó, các truy vấn `findBy*` là bất đồng bộ và trả về một Promise sẽ được giải quyết khi tìm thấy phần tử khớp. Chúng hữu ích cho việc kiểm thử các hoạt động bất đồng bộ dẫn đến thay đổi trong DOM.
Kiểm thử Hooks
React Hooks là các hàm có thể tái sử dụng để đóng gói logic có trạng thái. RTL cung cấp tiện ích `renderHook` từ `@testing-library/react-hooks` (đã không còn được dùng nữa thay vào đó là `@testing-library/react` trực tiếp kể từ v17) để kiểm thử các Hook tùy chỉnh một cách riêng biệt.
// src/hooks/useCounter.js
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => {
setCount(prevCount => prevCount + 1);
};
const decrement = () => {
setCount(prevCount => prevCount - 1);
};
return { count, increment, decrement };
}
export default useCounter;
// src/hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
test('increments the counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Giải thích:
- `renderHook`: Hàm này render Hook và trả về một đối tượng chứa kết quả của Hook.
- `act`: Hàm này được sử dụng để bọc bất kỳ mã nào gây ra cập nhật trạng thái. Điều này đảm bảo rằng React có thể nhóm và xử lý các cập nhật một cách chính xác.
Các Kỹ thuật Kiểm thử Nâng cao
Khi bạn đã thành thạo các kiến thức cơ bản về RTL, bạn có thể khám phá các kỹ thuật kiểm thử nâng cao hơn để cải thiện chất lượng và khả năng bảo trì của các bài test của mình.
Mock các Module
Đôi khi, bạn có thể cần phải mock các module hoặc dependency bên ngoài để cô lập các component của mình và kiểm soát hành vi của chúng trong quá trình kiểm thử. Jest cung cấp một API mock mạnh mẽ cho mục đích này.
// src/api/dataService.js
export const fetchData = async () => {
const response = await fetch('/api/data');
const data = await response.json();
return data;
};
// src/components/MyComponent.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../api/dataService';
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
// src/components/MyComponent.test.js
import { render, screen, waitFor } from '@testing-library/react';
import MyComponent from './MyComponent';
import * as dataService from '../api/dataService';
jest.mock('../api/dataService');
test('fetches data from the API', async () => {
dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' });
render(<MyComponent />);
await waitFor(() => screen.getByText('Mocked data!'));
expect(screen.getByText('Mocked data!')).toBeInTheDocument();
expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});
Giải thích:
- `jest.mock('../api/dataService')`: Dòng này mock module `dataService`.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: Dòng này cấu hình hàm `fetchData` đã được mock để trả về một Promise sẽ được giải quyết với dữ liệu được chỉ định.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Dòng này xác nhận rằng hàm `fetchData` đã được mock được gọi một lần.
Context Providers
Nếu component của bạn phụ thuộc vào một Context Provider, bạn sẽ cần bọc component của mình trong provider đó trong quá trình kiểm thử. Điều này đảm bảo rằng component có quyền truy cập vào các giá trị context.
// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// src/components/MyComponent.js
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div style={{ backgroundColor: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}>
<p>Current theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}
// src/components/MyComponent.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { ThemeProvider } from '../contexts/ThemeContext';
test('toggles the theme', () => {
render(
<ThemeProvider>
<MyComponent />
</ThemeProvider>
);
const themeParagraph = screen.getByText(/Current theme: light/i);
const toggleButton = screen.getByRole('button', { name: /Toggle Theme/i });
expect(themeParagraph).toBeInTheDocument();
fireEvent.click(toggleButton);
expect(screen.getByText(/Current theme: dark/i)).toBeInTheDocument();
});
Giải thích:
- Chúng ta bọc `MyComponent` trong `ThemeProvider` để cung cấp context cần thiết trong quá trình kiểm thử.
Kiểm thử với Router
Khi kiểm thử các component sử dụng React Router, bạn sẽ cần cung cấp một context Router giả. Bạn có thể đạt được điều này bằng cách sử dụng component `MemoryRouter` từ `react-router-dom`.
// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';
function MyComponent() {
return (
<div>
<Link to="/about">Go to About Page</Link>
</div>
);
}
// src/components/MyComponent.test.js
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MyComponent from './MyComponent';
test('renders a link to the about page', () => {
render(
<MemoryRouter>
<MyComponent />
</MemoryRouter>
);
const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', '/about');
});
Giải thích:
- Chúng ta bọc `MyComponent` trong `MemoryRouter` để cung cấp một context Router giả.
- Chúng ta xác nhận rằng phần tử liên kết có thuộc tính `href` chính xác.
Các Phương pháp Tốt nhất để Viết Test Hiệu quả
Dưới đây là một số phương pháp tốt nhất để tuân theo khi viết test với RTL:
- Tập trung vào Tương tác của Người dùng: Viết các bài test mô phỏng cách người dùng tương tác với ứng dụng của bạn.
- Tránh Kiểm thử Chi tiết Triển khai: Đừng kiểm tra hoạt động bên trong của các component của bạn. Thay vào đó, hãy tập trung vào hành vi có thể quan sát được.
- Viết các Bài Test Rõ ràng và Ngắn gọn: Làm cho các bài test của bạn dễ hiểu và dễ bảo trì.
- Sử dụng Tên Test có Ý nghĩa: Chọn tên test mô tả chính xác hành vi đang được kiểm thử.
- Giữ các Bài Test được Cô lập: Tránh sự phụ thuộc giữa các bài test. Mỗi bài test nên độc lập và tự chứa.
- Kiểm thử các Trường hợp Biên: Đừng chỉ kiểm thử trường hợp lý tưởng. Hãy đảm bảo kiểm thử cả các trường hợp biên và điều kiện lỗi.
- Viết Test trước khi Viết Mã: Cân nhắc sử dụng Phát triển Hướng Kiểm thử (TDD) để viết test trước khi bạn viết mã.
- Tuân thủ Mẫu "AAA": Arrange, Act, Assert (Sắp xếp, Hành động, Xác nhận). Mẫu này giúp cấu trúc các bài test của bạn và làm cho chúng dễ đọc hơn.
- Giữ cho các bài test của bạn nhanh: Các bài test chạy chậm có thể làm nản lòng các nhà phát triển trong việc chạy chúng thường xuyên. Tối ưu hóa tốc độ các bài test của bạn bằng cách mock các yêu cầu mạng và giảm thiểu lượng thao tác DOM.
- Sử dụng thông báo lỗi mang tính mô tả: Khi các xác nhận thất bại, thông báo lỗi nên cung cấp đủ thông tin để nhanh chóng xác định nguyên nhân của sự cố.
Kết luận
React Testing Library là một công cụ mạnh mẽ để viết các bài test hiệu quả, dễ bảo trì và tập trung vào người dùng cho các ứng dụng React của bạn. Bằng cách tuân theo các nguyên tắc và kỹ thuật được nêu trong hướng dẫn này, bạn có thể xây dựng các ứng dụng mạnh mẽ và đáng tin cậy đáp ứng nhu cầu của người dùng. Hãy nhớ tập trung vào việc kiểm thử từ góc nhìn của người dùng, tránh kiểm thử các chi tiết triển khai và viết các bài test rõ ràng và ngắn gọn. Bằng cách áp dụng RTL và các phương pháp tốt nhất, bạn có thể cải thiện đáng kể chất lượng và khả năng bảo trì của các dự án React của mình, bất kể vị trí của bạn hay các yêu cầu cụ thể của khán giả toàn cầu của bạn.