React Testing Library假定时器下组件未及时重渲染问题求助
React组件假定时器测试:Promise与重渲染同步问题
问题场景
我正在测试一个组件,它会在1秒延迟后调用传入的回调函数(返回Promise),并根据Promise结果更新显示内容。组件在浏览器中运行正常,但使用Jest假定时器测试时遇到以下问题:
- 推进定时器到足够时间后,组件并未立即重渲染
- Promise的resolve回调要等到测试断言失败后才会执行
- 尝试
await new Promise(process.nextTick)会和假定时器死锁,waitFor也无法触发回调执行
组件代码
import React, { useEffect, useState } from 'react'; export interface TestComponentProps { callback: () => Promise<string> } const TestComponent: React.FC<TestComponentProps> = ({ callback, }: TestComponentProps) => { const [textDisplay, setTextDisplay] = useState('not resolved'); const [timeLeft, setTimeLeft] = useState(1000); const [timerDone, setTimerDone] = useState(false); const timeStart = Date.now(); const receiveText = async (text: Promise<string>) => { setTextDisplay(await text); } useEffect(() => { if (timerDone) { return () => undefined; } // Update screen every 50 ms const interval = setInterval(() => { const newTimeLeft = 1000 - Date.now() + timeStart; setTimeLeft(newTimeLeft); if (newTimeLeft <= 0) { setTimerDone(true); receiveText(callback()); } }, 50); return () => clearInterval(interval); }, [timerDone]); if (!timerDone) { return ( <p>{timeLeft} ms to go</p> ); } else { return ( <p>{textDisplay}</p> ) } }; export default TestComponent;
原测试代码
import { render, screen, act } from '@testing-library/react'; import React from 'react'; import TestComponent from './TestComponent'; it('renders the required information after the promise resolves', () => { jest.useFakeTimers(); const callback = jest.fn().mockResolvedValue('Hello world'); render(<TestComponent callback={callback} />); // Now wait for the timer to finish - give it an extra 50ms just in case act(() => jest.advanceTimersByTime(1050)); // The promise should have resolved and the text should have appeared in the document expect(screen.getByText('Hello world')).toBeInTheDocument(); // The promise only resolves HERE // after the test will already have failed jest.clearAllTimers(); jest.useRealTimers(); });
解决方案
核心原因
Jest假定时器会阻塞JavaScript的微任务队列,Promise的resolve回调属于微任务,不会自动执行,必须手动触发。
修改后的测试代码
import { render, screen, act } from '@testing-library/react'; import React from 'react'; import TestComponent from './TestComponent'; it('renders the required information after the promise resolves', () => { jest.useFakeTimers(); const callback = jest.fn().mockResolvedValue('Hello world'); render(<TestComponent callback={callback} />); // 推进定时器到触发回调的时间点 act(() => { jest.advanceTimersByTime(1050); }); // 手动执行所有待处理的微任务,让Promise完成resolve jest.runAllMicrotasks(); // 再次调用act确保React完成所有状态更新和重渲染 act(() => {}); // 此时断言就能获取到最新的组件状态 expect(screen.getByText('Hello world')).toBeInTheDocument(); expect(callback).toHaveBeenCalled(); jest.clearAllTimers(); jest.useRealTimers(); });
组件逻辑优化(可选)
原组件中timeStart = Date.now()会在每次组件重渲染时重新赋值,在假定时器环境下会导致时间计算错误。可以用useRef保存初始时间,避免重置:
// 替换原timeStart定义 const timeStart = useRef(Date.now()); // 使用时取current属性 const newTimeLeft = 1000 - Date.now() + timeStart.current;
这个优化能让组件在假定时器下的时间计算更准确,避免因重渲染导致的逻辑偏差。
额外提示
如果不需要实时更新倒计时,建议用setTimeout代替setInterval,减少定时器触发次数,测试时也无需依赖interval的执行间隔:
useEffect(() => { if (timerDone) return; const timer = setTimeout(() => { setTimerDone(true); receiveText(callback()); }, 1000); return () => clearTimeout(timer); }, [timerDone]);
内容的提问来源于stack exchange,提问作者Maddy Guthridge
相关产品推荐
相关产品推荐

