如何使用RTL的rerender测试useEffect内部的动作?
问题场景与困惑
我有如下React组件:
const Example = ({ prop1, prop2 }) => { useEffect(() => { // ...其他逻辑 someFunctionCall(arg) }, [prop1]) useEffect(() => { // ...其他逻辑 someFunctionCall(arg) }, [prop2]) // ...组件其他内容 }
我想要测试:当修改prop1时,仅触发一次新的someFunctionCall调用。于是写了如下测试代码:
it('when prop1 changes', () => { const { rerender } = render(<Example prop1='value1' prop2='value2' />) // 首次渲染时两个useEffect都会执行 expect(someFunctionCall).toBeCalledTimes(2) rerender(<Example prop1='newValue' prop2='value2' />) // 预期prop1更新时仅第一个useEffect执行,累计调用3次 expect(someFunctionCall).toBeCalledTimes(3) })
但测试失败了:函数被调用了4次。查了原因发现,RTL的rerender并非只更新props,而是用新props重新渲染整个组件,导致两个useEffect再次执行,和真实系统的props更新行为不符。请问该如何用RTL正确测试这种props变更的功能?
解决方案
1. 重置Mock调用计数(最直接的解决方式)
问题出在你一直在累计统计someFunctionCall的调用次数,正确做法是在rerender前清空mock的历史调用记录,只统计props变更后的调用次数。修改后的测试代码如下:
it('当prop1变更时,仅触发一次新的函数调用', () => { const { rerender } = render(<Example prop1='value1' prop2='value2' />) // 验证首次渲染的调用次数 expect(someFunctionCall).toBeCalledTimes(2) // 清空mock的历史调用记录,只关注后续rerender的行为 someFunctionCall.mockClear() rerender(<Example prop1='newValue' prop2='value2' />) // 此时仅依赖prop1的useEffect会执行,所以调用次数为1 expect(someFunctionCall).toBeCalledTimes(1) })
2. 排查组件逻辑是否存在不稳定依赖
需要确认:你的组件本身逻辑是否符合预期。RTL的rerender本质是完全模拟React的props更新流程,和真实场景下的props更新行为一致。如果出现两个useEffect都重新执行的情况,很可能是arg存在不稳定依赖——比如arg是每次渲染都会重新创建的对象、数组或函数,导致即使prop没有变化,useEffect也会被触发。
这种情况下需要用useCallback或useMemo缓存arg,确保依赖数组的稳定性,比如:
const Example = ({ prop1, prop2 }) => { // 用useMemo缓存arg,避免每次渲染重新创建 const arg = useMemo(() => ({ /* 你的arg内容 */ }), []) useEffect(() => { someFunctionCall(arg) }, [prop1, arg]) useEffect(() => { someFunctionCall(arg) }, [prop2, arg]) // ...组件其他内容 }
3. 更细致的调用验证(可选)
如果需要确认rerender后的调用确实来自依赖prop1的useEffect,可以通过mock的mock.calls数组验证调用参数,或者在useEffect中传入不同的标识参数:
组件中修改:
useEffect(() => { someFunctionCall('prop1-triggered', arg) }, [prop1]) useEffect(() => { someFunctionCall('prop2-triggered', arg) }, [prop2])
测试中验证:
// rerender后检查调用参数 expect(someFunctionCall).toHaveBeenCalledWith('prop1-triggered', expect.anything()) // 确保没有来自prop2的调用 expect(someFunctionCall).not.toHaveBeenCalledWith('prop2-triggered', expect.anything())
内容的提问来源于stack exchange,提问作者Ben Graham
相关产品推荐
相关产品推荐

