Kiểm thử debounce và throttle trong React với Jest
Implementing debounce hoặc throttle trong React là một yêu cầu phổ biến khi làm việc với các input, sự kiện cuộn (scroll) và tìm kiếm. Tuy nhiên, việc kiểm thử những hành vi này có thể gặp khó khăn, đặc biệt là khi liên quan đến thời gian, timers bất đồng bộ và side effects.
Mục tiêu của bài viết
Trong bài viết này, bạn sẽ học được:
- Sự khác biệt giữa debounce và throttle
- Cách để triển khai chúng trong React
- Cách kiểm thử chúng bằng Jest và @testing-library/react
- Cách sử dụng
jest.useFakeTimers()một cách hiệu quả
🔍 Debounce và Throttle là gì?
🐢 Debounce
debounce là một kỹ thuật mà trì hoãn việc thực thi một hàm cho đến khi một khoảng thời gian xác định đã trôi qua kể từ lần gọi cuối cùng.
🧠 Thích hợp cho: tìm kiếm khi gõ, tự động lưu (auto-saves), v.v.
🐇 Throttle
throttle đảm bảo rằng một hàm chỉ được thực thi một lần trong một khoảng thời gian nhất định, bỏ qua những lần gọi tiếp theo trong khoảng thời gian này.
🧠 Thích hợp cho: sự kiện cuộn (scroll), thay đổi kích thước (resize), nhấn nút nhanh, v.v.
🧱 Triển khai Debounce trong React
Dưới đây là cách triển khai debounce trong một component tìm kiếm:
typescript
// SearchInput.tsx
import { useEffect, useState } from 'react';
type Props = {
onSearch: (query: string) => void;
};
export function SearchInput({ onSearch }: Props) {
const [value, setValue] = useState('');
useEffect(() => {
const timeout = setTimeout(() => {
onSearch(value);
}, 500);
return () => clearTimeout(timeout);
}, [value, onSearch]);
return (
<input
placeholder="Tìm kiếm..."
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
🧪 Kiểm thử Debounce với jest.useFakeTimers()
typescript
// SearchInput.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { SearchInput } from './SearchInput';
jest.useFakeTimers();
test('gọi onSearch sau độ trễ debounce', () => {
const handleSearch = jest.fn();
render(<SearchInput onSearch={handleSearch} />);
const input = screen.getByPlaceholderText(/tìm kiếm/i);
fireEvent.change(input, { target: { value: 'xin chào' } });
// Chưa gọi hàm
expect(handleSearch).not.toHaveBeenCalled();
// Tiến tới 500ms
jest.advanceTimersByTime(500);
expect(handleSearch).toHaveBeenCalledWith('xin chào');
});
⚡ Triển khai Throttle với Lodash
Dưới đây là cách triển khai throttle trong một nút bấm:
typescript
// ThrottledButton.tsx
import { throttle } from 'lodash';
import { useCallback } from 'react';
export function ThrottledButton({ onClick }: { onClick: () => void }) {
const throttled = useCallback(throttle(onClick, 1000), [onClick]);
return <button onClick={throttled}>Nhấn nhanh!</button>;
}
🧪 Kiểm thử Throttle với Fake Timers
typescript
// ThrottledButton.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { ThrottledButton } from './ThrottledButton';
jest.useFakeTimers();
test('gọi onClick chỉ một lần trong khoảng thời gian throttle', () => {
const handleClick = jest.fn();
render(<ThrottledButton onClick={handleClick} />);
const button = screen.getByText(/nhấn nhanh/i);
fireEvent.click(button);
fireEvent.click(button);
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(1000);
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(2);
});
🧠 Mẹo nhanh
- Sử dụng
jest.useFakeTimers()trước khi render component. - Luôn kết thúc bằng
jest.useRealTimers()nếu bạn muốn thực hiện các bài kiểm thử khác sau đó. - Debounce có thể được kiểm thử với
setTimeout; throttle yêu cầu thư viện nhưlodashhoặclodash.throttle.
✅ Kết luận
Việc kiểm thử debounce và throttle dễ dàng hơn bạn nghĩ khi bạn sử dụng các công cụ phù hợp: fake timers + sự kiện mô phỏng. Đừng chỉ dựa vào hành vi trực quan — việc viết các bài kiểm thử vững chắc sẽ đảm bảo rằng input của bạn không phát sinh các gọi không cần thiết trong tương lai.
Câu hỏi thường gặp (FAQ)
1. Debounce và throttle có thể sử dụng trong trường hợp nào?
Debounce thường được sử dụng trong các tình huống như tìm kiếm khi gõ, trong khi throttle thường được sử dụng cho các sự kiện như cuộn và thay đổi kích thước.
2. Làm thế nào để kiểm thử debounce trong React?
Bạn có thể sử dụng jest.useFakeTimers() để kiểm thử debounce bằng cách mô phỏng thời gian và kiểm tra xem hàm đã được gọi sau thời gian trì hoãn hay chưa.
3. Có cần thiết phải sử dụng thư viện bên ngoài cho throttle không?
Đúng, thường thì bạn nên sử dụng lodash hoặc các thư viện tương tự để triển khai throttle một cách hiệu quả.