如何使用Jest + Enzyme为定时器与useEffect Hook编写测试用例
React组件:getNews异步函数与定时器逻辑的测试用例编写
测试前的准备工作
- 用到的工具:
@testing-library/react(组件渲染与查询)、jest(测试框架) - 关键mock:
- 用
jest.mock('axios')mock所有axios请求,避免真实网络请求干扰测试 - 开启Jest假定时器:
jest.useFakeTimers(),不用等真实时间就能模拟定时器触发
- 用
一、getNews异步函数的测试
1. 测试page=0时的初始数据加载
验证首次加载时,newsData被直接替换为接口返回的hits,同时totalPage和loader状态更新正确。
import { render, waitFor, screen } from '@testing-library/react'; import axios from 'axios'; import NewsComponent from './NewsComponent'; jest.mock('axios'); test('getNews(page=0) 正确初始化数据与状态', async () => { // 模拟接口返回的测试数据 const testHits = [{ id: 1, title: '测试新闻1' }]; const testTotalPages = 8; (axios.get as jest.Mock).mockResolvedValue({ data: { hits: testHits, nbPages: testTotalPages } }); // 渲染组件 render(<NewsComponent />); // 等待异步请求完成,断言状态更新结果 await waitFor(() => { // 假设新闻项有data-testid="news-item"属性,验证内容渲染 expect(screen.getByTestId('news-item')).toHaveTextContent('测试新闻1'); // 验证总页数显示正确 expect(screen.getByTestId('total-pages')).toHaveTextContent('8'); // 验证loader最终为true(假设loader显示时才有对应元素) expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument(); }); });
2. 测试page>0时的追加数据逻辑
当page大于0时,newsData应为原有数据加上新请求的hits,验证这个追加逻辑是否正常。
import React from 'react'; import { render } from '@testing-library/react'; import axios from 'axios'; import NewsComponent from './NewsComponent'; jest.mock('axios'); test('getNews(page>0) 正确追加新闻数据', async () => { // 模拟两次接口返回:第一次初始数据,第二次新数据 const initialHits = [{ id: 1, title: '初始新闻' }]; const newHits = [{ id: 2, title: '新新闻' }]; (axios.get as jest.Mock) .mockResolvedValueOnce({ data: { hits: initialHits, nbPages: 10 } }) .mockResolvedValueOnce({ data: { hits: newHits, nbPages: 10 } }); // Mock useState的setNewsData,方便验证更新逻辑 const mockSetNewsData = jest.fn(); jest.spyOn(React, 'useState').mockImplementationOnce(() => [initialHits, mockSetNewsData]); render(<NewsComponent />); // 模拟调用getNews(1)(若函数未暴露,可通过组件内部逻辑触发,比如手动模拟定时器触发) await getNews(1); // 验证setNewsData被调用,且传入的回调函数能正确追加数据 expect(mockSetNewsData).toHaveBeenCalledWith(expect.any(Function)); const updateFn = mockSetNewsData.mock.calls[0][0]; expect(updateFn(initialHits)).toEqual([...initialHits, ...newHits]); });
3. 测试请求失败时的loader状态
接口请求失败时,确保loader会被设置为true,避免页面一直处于加载状态。
import { render, waitFor, screen } from '@testing-library/react'; import axios from 'axios'; import NewsComponent from './NewsComponent'; jest.mock('axios'); test('getNews请求失败时,loader正确恢复为true', async () => { // 模拟请求失败 (axios.get as jest.Mock).mockRejectedValue(new Error('接口请求失败')); render(<NewsComponent />); await waitFor(() => { // 验证loader最终隐藏(假设加载状态显示时才有对应元素) expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument(); }); });
二、useEffect中定时器逻辑的测试
1. 测试定时器按时触发getNews
验证定时器每隔1秒调用一次getNews,直到i达到49(因为i<50时才调用)。
import { render, waitFor } from '@testing-library/react'; import axios from 'axios'; import NewsComponent from './NewsComponent'; jest.mock('axios'); test('定时器每隔1秒调用getNews,直到i=49', async () => { // 模拟接口返回,避免异步请求阻塞测试 (axios.get as jest.Mock).mockResolvedValue({ data: { hits: [], nbPages: 10 } }); // Mock getNews函数,统计调用次数(函数式组件可通过mock useState间接验证) const getNewsSpy = jest.spyOn(NewsComponent, 'getNews'); render(<NewsComponent />); // 快进1秒,触发第一次调用(i=1) jest.advanceTimersByTime(1000); await waitFor(() => expect(getNewsSpy).toHaveBeenCalledTimes(1)); // 再快进48秒,总共触发49次调用(i从1到49) jest.advanceTimersByTime(48000); await waitFor(() => expect(getNewsSpy).toHaveBeenCalledTimes(49)); });
2. 测试达到limit后定时器停止
当i等于50时,定时器应该被清除,不再调用getNews。
import { render, waitFor } from '@testing-library/react'; import axios from 'axios'; import NewsComponent from './NewsComponent'; jest.mock('axios'); test('i达到50时,定时器停止调用getNews', async () => { (axios.get as jest.Mock).mockResolvedValue({ data: { hits: [], nbPages: 10 } }); const getNewsSpy = jest.spyOn(NewsComponent, 'getNews'); render(<NewsComponent />); // 快进50秒,此时i=50,定时器应该已经停止 jest.advanceTimersByTime(50000); await waitFor(() => expect(getNewsSpy).toHaveBeenCalledTimes(49)); // 再快进1秒,确认没有新的调用 jest.advanceTimersByTime(1000); expect(getNewsSpy).toHaveBeenCalledTimes(49); });
3. 测试组件卸载时清理定时器
组件卸载时必须清除定时器,避免内存泄漏,验证这个清理逻辑。
import { render } from '@testing-library/react'; import NewsComponent from './NewsComponent'; test('组件卸载时自动清除定时器', () => { // Mock clearInterval,验证是否被调用 const clearIntervalSpy = jest.spyOn(window, 'clearInterval'); const { unmount } = render(<NewsComponent />); // 卸载组件 unmount(); // 确认clearInterval被调用,定时器已清理 expect(clearIntervalSpy).toHaveBeenCalled(); });
额外提示
- 函数式组件中内部函数(比如getNews)无法直接spy时,可通过mock useState的更新函数间接验证逻辑,比如监听setNewsData的调用参数
- 所有异步操作必须用
waitFor或findBy*系列查询方法,确保状态更新完成后再执行断言 - 测试完成后记得恢复真实定时器:
jest.useRealTimers(),避免影响其他测试
内容的提问来源于stack exchange,提问作者user2810894
相关产品推荐
相关产品推荐

