Custom Hooks thường chứa logic nghiệp vụ phức tạp. Test chúng độc lập giúp bạn tự tin tái sử dụng.
Thư viện RTL cung cấp renderHook cho việc này.
1. Ví dụ: useCounter
ts:
import { useState, useCallback } from 'react';
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount((x) => x + 1), []);
return { count, increment };
}2. Viết Test
ts:
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());
// result.current là giá trị trả về mới nhất của hook
expect(result.current.count).toBe(0);
});
it('should increment', () => {
const { result } = renderHook(() => useCounter(10));
// ⚠️ QUAN TRỌNG: Mọi hành động update state phải bọc trong act()
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(11);
});
});3. Tại sao phải dùng act?
React đảm bảo mọi update state được hoàn tất và DOM được cập nhật trước khi chạy dòng lệnh tiếp theo.
Khi test, môi trường giả lập không có cơ chế loop này tự động. act giúp giả lập việc "xả" các update vào DOM/State.
Nếu không dùng act, bạn sẽ gặp warning đỏ lòm console: "An update to Component inside a test was not wrapped in act...".
Nên nhớ:
userEventcủa RTL đã tự bọcactbên trong.renderHookupdate state thủ công thì BẮT BUỘC dùngact.
4. Test Hook có Async (API)
ts:
it('should load user data', async () => {
const { result } = renderHook(() => useUser('123'));
// Chờ cho đến khi loading = false
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.data).toEqual({ name: 'John' });
});