如何用Jest和Testing Library测试addEventListener?自定义Hook测试失败
问题
我在React中写了一个自定义Hook useDynamicDimensions,用来监听元素尺寸变化并返回宽高:
export function useDynamicDimensions(element) { const [ width, setWidth ] = useState(element?.innerWidth); const [ height, setHeight ] = useState(element?.innerHeight); useEffect(() => { const handleWindowResize = () => { setWidth(element?.innerWidth); setHeight(element?.innerHeight); }; element?.addEventListener('resize', handleWindowResize); return () => element?.removeEventListener('resize', handleWindowResize); }, []); return { width, height }; }
用Jest和Testing Library写了测试用例:
it('updates dimensions on window resize', () => { const initialWidth = 100; const initialHeight = 200; const newWidth = 250; const newHeight = 300; const element = { innerWidth: initialWidth, innerHeight: initialHeight, addEventListener: jest.fn(), removeEventListener: jest.fn(), }; const { result } = renderHook(() => useDynamicDimensions(element)); expect(result.current.width).toBe(initialWidth); expect(result.current.height).toBe(initialHeight); act(() => { element.innerWidth = newWidth; element.innerHeight = newHeight; global.dispatchEvent(new Event('resize')); }); expect(result.current.width).toBe(250); });
但测试失败,报错:
expect(received).toBe(expected) Expected: 250 Received: 100
错误原因及修复方案
核心问题
- 闭包陷阱导致读取旧值:
useEffect的依赖数组为空,handleWindowResize函数会捕获初始的element引用,后续修改element的innerWidth/innerHeight时,函数里读取的还是旧的属性值,不会触发状态更新。 - 事件触发对象不匹配:Hook监听的是传入
element的resize事件,但测试里触发的是global(window)的resize事件,两者不是同一个对象,回调不会执行。 - Mock事件未实际绑定回调:测试中用
jest.fn()模拟了addEventListener,需要手动获取并触发监听的回调,而不是dispatch全局事件。
修复Hook
方案一:将element加入useEffect依赖数组,确保依赖变化时重新绑定事件:
export function useDynamicDimensions(element) { const [ width, setWidth ] = useState(element?.innerWidth); const [ height, setHeight ] = useState(element?.innerHeight); useEffect(() => { if (!element) return; const handleResize = () => { setWidth(element.innerWidth); setHeight(element.innerHeight); }; element.addEventListener('resize', handleResize); return () => element.removeEventListener('resize', handleResize); }, [element]); // 添加element到依赖数组 return { width, height }; }
方案二:用useRef保存element,避免频繁解绑绑定(适合element频繁变化的场景):
export function useDynamicDimensions(element) { const [ width, setWidth ] = useState(element?.innerWidth); const [ height, setHeight ] = useState(element?.innerHeight); const elementRef = useRef(element); // 实时更新ref指向最新的element useEffect(() => { elementRef.current = element; }, [element]); useEffect(() => { const handleResize = () => { setWidth(elementRef.current?.innerWidth); setHeight(elementRef.current?.innerHeight); }; const currentElement = elementRef.current; if (!currentElement) return; currentElement.addEventListener('resize', handleResize); return () => currentElement.removeEventListener('resize', handleResize); }, []); return { width, height }; }
修复测试用例
获取mock的addEventListener中的回调函数,手动触发它:
it('updates dimensions on element resize', () => { const initialWidth = 100; const initialHeight = 200; const newWidth = 250; const newHeight = 300; const element = { innerWidth: initialWidth, innerHeight: initialHeight, addEventListener: jest.fn(), removeEventListener: jest.fn(), }; const { result } = renderHook(() => useDynamicDimensions(element)); expect(result.current.width).toBe(initialWidth); expect(result.current.height).toBe(initialHeight); // 获取监听的resize回调函数 const handleResize = element.addEventListener.mock.calls[0][1]; act(() => { element.innerWidth = newWidth; element.innerHeight = newHeight; // 触发element的resize回调 handleResize(); }); expect(result.current.width).toBe(newWidth); expect(result.current.height).toBe(newHeight); });
内容的提问来源于stack exchange,提问作者Jake Cano
相关产品推荐
相关产品推荐

