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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 18:44:53