如何用Jest测试React组件等待API响应时words数组为空?
问题:测试React组件在API请求等待期间的状态变化
组件代码
const [words, setWords] = useState(['Word','Another']); const handleSelection = async selection => { setWords([]); const { response } = await apiCall(selection); setWords(response.split(' ')); }; return ( <div> {words.map(word => ( <p onClick={() => handleSelection(word)} data-testid='words' > {word} </p> ))} </div> )
额外说明:组件其他位置有1500ms的setTimeout,通过useEffect在初始加载时执行。
当前测试代码
beforeEach(() => { apiCall.mockImplementation(() => ({ response: 'This is the response' })); }); it("words disappear whilst awaiting api response", async () => { jest.useFakeTimers(); const { getAllByTestId, queryAllByTestId } = render(<Comp />); const user = userEvent.setup({ delay: null }); act(() => { jest.runAllTimers(); }); await act(async () => { await user.click(getAllByTestId('words')[0]) }); expect(getAllByTestId('words')).toHaveLength(0); jest.useRealTimers(); });
测试失败结果
Expected length: 0 Received length: 4 Received array: [<p data-testid='words'>This</p>,<p data-testid='words'>is</p>,<p data-testid='words'>the</p>,<p data-testid='words'>response</p>]
需求:需要验证在等待API响应时,words数组会变为空状态。尝试过给apiCall的mock添加setTimeout并推进计时器到响应未返回的时间点,但未成功。
解决方案
核心问题是你的apiCall mock是同步返回结果的,导致handleSelection里的await直接跳过,setWords(response.split(' '))立刻执行,根本没捕捉到中间的空数组状态。要测试中间状态,需要让apiCall返回一个未resolved的Promise,在触发点击后先断言空状态,再手动控制Promise的resolve时机。
方法一:手动控制API Promise的resolve(推荐)
这种方式无需依赖计时器,直接控制API响应的返回时机,更灵活可靠:
it("words disappear whilst awaiting api response", async () => { jest.useFakeTimers(); // 定义可手动控制的Promise,保存resolve函数 let resolveApiCall; apiCall.mockImplementation(() => { return new Promise(resolve => { resolveApiCall = resolve; }); }); const { getAllByTestId, queryAllByTestId } = render(<Comp />); const user = userEvent.setup({ delay: null }); // 处理初始加载的1500ms setTimeout act(() => { jest.runAllTimers(); }); // 验证初始状态的words数量 expect(getAllByTestId('words')).toHaveLength(2); // 触发点击,注意不要用await,避免等待API返回 user.click(getAllByTestId('words')[0]); // 等待组件更新,断言中间空状态 await waitFor(() => { expect(queryAllByTestId('words')).toHaveLength(0); }); // 可选:验证API响应返回后的最终状态 resolveApiCall({ response: 'This is the response' }); await waitFor(() => { expect(queryAllByTestId('words')).toHaveLength(4); }); jest.useRealTimers(); });
方法二:模拟API延迟并控制计时器推进
如果需要模拟真实的API延迟场景,可以给mock添加setTimeout,然后控制计时器推进到响应未返回的时间点:
it("words disappear whilst awaiting api response", async () => { jest.useFakeTimers(); // 模拟API延迟200ms返回 apiCall.mockImplementation(() => { return new Promise(resolve => { setTimeout(() => { resolve({ response: 'This is the response' }); }, 200); }); }); const { getAllByTestId, queryAllByTestId } = render(<Comp />); const user = userEvent.setup({ delay: null }); // 处理初始加载的定时器 act(() => { jest.runAllTimers(); }); expect(getAllByTestId('words')).toHaveLength(2); // 触发点击 user.click(getAllByTestId('words')[0]); // 推进计时器到API还未返回的时间点(199ms) act(() => { jest.advanceTimersByTime(199); }); // 断言空状态 await waitFor(() => { expect(queryAllByTestId('words')).toHaveLength(0); }); // 推进到API返回的时间点,验证最终状态 act(() => { jest.advanceTimersByTime(1); }); await waitFor(() => { expect(queryAllByTestId('words')).toHaveLength(4); }); jest.useRealTimers(); });
关键注意点
- 使用React Testing Library的
waitFor工具,它会自动等待组件状态更新完成,比手动嵌套act更可靠。 - 触发点击时不要用
await,否则会等待整个异步流程(包括API返回)完成,无法捕捉中间状态。
内容的提问来源于stack exchange,提问作者abbyabby
相关产品推荐
相关产品推荐

