如何在React Testing Library中正确Mock多个useRef测试自定义滚动Hook?
自定义Hook单元测试问题:useScrollArea的测试修复方案
问题背景
我开发了一个用于构建自定义滚动组件的自定义Hook,功能正常,但单元测试无法覆盖,怀疑是useRef的Mock方式有问题,也可能存在其他遗漏点。尝试了大量文章和帖子都没解决,持续遇到TypeError: Cannot read properties of undefined (reading 'current')错误,或者Hook返回结果不符合预期。
Hook代码
export const useScrollArea = () => { const containerRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null); const scrollBarThumbRef = useRef<HTMLDivElement>(null); const [isScrollable, setIsScrollable] = useState(false); const isOverflowing = useCallback(() => { if (!containerRef.current || !contentRef.current) { return false; } const isContentOverflowing = contentRef.current.scrollHeight > containerRef.current.clientHeight; setIsScrollable(isContentOverflowing); }, []); const updateThumbPosition = useCallback(() => { const scrollRatio = containerRef.current.scrollTop / (contentRef.current.scrollHeight - containerRef.current.clientHeight); const thumbTop = scrollRatio * (containerRef.current.clientHeight - scrollBarThumbRef.current.clientHeight); scrollBarThumbRef.current.style.transform = `translateY(${thumbTop}px)`; }, []); const scroll = (direction: ScrollDirection) => { if ( !containerRef?.current || !contentRef?.current || !scrollBarThumbRef?.current || !isScrollable ) { return; } const maxScrollPosition = contentRef.current.scrollHeight - containerRef.current.clientHeight; let newScrollPosition = containerRef.current.scrollTop + (direction === 'up' ? -1 : 1) * SCROLL_STEP; newScrollPosition = Math.max( 0, Math.min(newScrollPosition, maxScrollPosition) ); containerRef.current.scroll({ top: newScrollPosition }); updateThumbPosition(); }; useEffect(() => { isOverflowing(); }, [isOverflowing]); return { scroll, isScrollable, contentRef, containerRef, scrollBarThumbRef }; };
现有测试代码
const mockContainerRef = { current: { clientHeight: 100, scrollTop: 0, scroll: jest.fn() } }; const mockContentRef = { current: { scrollHeight: 200 } }; const mockScrollBarThumbRef = { current: { clientHeight: 50, style: { transform: '' } } }; jest.mock('react', () => ({ ...jest.requireActual('react'), useRef: jest .fn() .mockImplementationOnce(() => mockContainerRef) .mockImplementationOnce(() => mockContentRef) .mockImplementationOnce(() => mockScrollBarThumbRef) })); describe('useScrollArea', () => { it('should return `isScrollable` as true when content is overflowing', () => { const { result } = renderHook(() => useScrollArea()); expect(result.current.isScrollable).toBe(true); }); it('should return `isScrollable` as false when content is not overflowing', () => { mockContentRef.current.scrollHeight = 50; const { result } = renderHook(() => useScrollArea()); expect(result.current.isScrollable).toBe(false); }); it('should scroll down', () => { mockContainerRef.current.scrollTop = 100; const { result } = renderHook(() => useScrollArea()); act(() => { result.current.scroll('down'); }); expect(mockContainerRef.current.scroll).toHaveBeenCalledWith({ top: 40 }); }); });
遇到的错误
TypeError: Cannot read properties of undefined (reading 'current')
问题分析与修复方案
1. 修复useRef的Mock复用问题
你使用mockImplementationOnce只会在第一次调用useRef时返回对应mock,后续测试用例中useRef的调用会因为没有后续mock定义而返回undefined,直接导致current读取失败。
修复方式:
让useRef的mock按调用顺序循环返回三个ref,同时在每个测试用例前重置mock和ref的初始状态:
// 替换原jest.mock部分 jest.mock('react', () => { const originalReact = jest.requireActual('react'); return { ...originalReact, useRef: jest.fn(() => { const refs = [mockContainerRef, mockContentRef, mockScrollBarThumbRef]; const currentIndex = (useRef.mock.calls.length - 1) % refs.length; return refs[currentIndex]; }) }; }); // 在每个测试用例前重置mock和ref状态 beforeEach(() => { jest.clearAllMocks(); // 重置ref的初始值 mockContainerRef.current = { clientHeight: 100, scrollTop: 0, scroll: jest.fn() }; mockContentRef.current = { scrollHeight: 200 }; mockScrollBarThumbRef.current = { clientHeight: 50, style: { transform: '' } }; });
2. 补充SCROLL_STEP常量定义
测试代码中未定义SCROLL_STEP常量,导致scroll函数计算时出错,这也是引发错误的潜在原因。
修复方式:
在测试文件顶部添加和Hook中一致的常量定义:
const SCROLL_STEP = 60; // 替换为你Hook中实际使用的数值
3. 修正测试用例逻辑
- 第二个测试用例中,修改
mockContentRef.current.scrollHeight后,要确保在renderHook前完成设置,避免闭包读取旧值; - 第三个测试用例的预期值错误:当
scrollTop初始为100,maxScrollPosition为100(200-100)时,向下滚动不会超过最大值,预期值应调整为100而非40。
修正后的测试用例示例:
it('should return `isScrollable` as false when content is not overflowing', () => { // 在renderHook前设置值 mockContentRef.current.scrollHeight = 50; const { result } = renderHook(() => useScrollArea()); expect(result.current.isScrollable).toBe(false); }); it('should scroll down and cap at max position', () => { mockContainerRef.current.scrollTop = 100; const { result } = renderHook(() => useScrollArea()); act(() => { result.current.scroll('down'); }); // 不会超过maxScrollPosition=100 expect(mockContainerRef.current.scroll).toHaveBeenCalledWith({ top: 100 }); });
4. 测试thumb位置更新逻辑
可以添加测试用例验证滚动时滚动条thumb的位置是否正确更新:
it('should update thumb position when scrolling', () => { mockContainerRef.current.scrollTop = 50; mockContentRef.current.scrollHeight = 200; mockContainerRef.current.clientHeight = 100; mockScrollBarThumbRef.current.clientHeight = 50; const { result } = renderHook(() => useScrollArea()); // 通过scroll触发updateThumbPosition act(() => { result.current.scroll('down'); }); // 计算:scrollRatio=50/(200-100)=0.5;thumbTop=0.5*(100-50)=25 expect(mockScrollBarThumbRef.current.style.transform).toBe('translateY(25px)'); });
内容的提问来源于stack exchange,提问作者t97
相关产品推荐
相关产品推荐

