You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 01:45:27