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

如何用Jest和React Testing Library在useEffect中Mock axios.CancelToken

React测试中Mock Axios CancelToken的解决方案

问题背景

  • 需求:通过React组件测试,该组件在useEffect中使用Axios的CancelToken实现请求取消逻辑
  • 尝试:使用jest.spyOn mock axios.CancelToken.source,但触发错误:Cannot spyOn on a primitive value; undefined given

组件代码

const MyComponent = () => {
  const [name, setName] = useState("");
  
  useEffect(() => {
    const cancelToken = axios.CancelToken;
    const source = cancelToken.source();
    
    const getName = async () => {
      try {
        const name = await fetchName(source.token);
        setName(name ? name : 'noName')
      } catch (error) {
        console.log(error)
      }
    };
    
    getName()
    
    return () => {
      source.cancel('axios request cancelled');
    };
  }, []);

  return <h1> Hello {name} </h1> 
}

原测试代码及错误信息

测试代码

/**
 * @jest-environment jsdom
 */
import axios from 'axios';

test('MyComponent', async () => {
  const cancelTokenSource: any = { cancel: jest.fn(), token: { reason: { message: 'user canceled' } } };
  jest.spyOn(axios.CancelToken, 'source').mockReturnValueOnce(cancelTokenSource);
  jest.spyOn(axios, 'get').mockResolvedValueOnce('joe');
  
  useRouter.mockImplementationOnce(() => ({
    query: { cid: 'coffee' },
  }))
  
  render(<MyComponent />);
 
  const element = await screen.findAllByText('joe');
  expect(element[0]).toBeInTheDocument();
})

错误信息

● Testing MyComponent › 

  Cannot spyOn on a primitive value; undefined given

  59 |     test('MyComponent...', async () => {
  60 |         const cancelTokenSource: any = { cancel: jest.fn(), token: { reason: { message: 'user canceled' } } };
> 61 |         jest.spyOn(axios.CancelToken, 'source').mockReturnValueOnce(cancelTokenSource);
     |              ^

依赖版本

"dependencies": {
  "axios": "^0.27.2",
  ....
},
"devDependencies": {
  "@testing-library/jest-dom": "^5.16.4",
  "@testing-library/react": "^13.3.0",
  "@testing-library/user-event": "^14.2.0",
  "@types/jest": "^28.1.1",
  "@types/node": "17.0.42",
  "jest": "^28.1.1",
  "jest-environment-jsdom": "^28.1.1",
  "jest-preview": "^0.2.8",
}

解决方案

报错原因是测试环境中axios.CancelToken为undefined,jest.spyOn无法对不存在的属性进行监听。以下两种方案可解决问题:

方案1:Mock整个Axios模块

通过jest.mock直接构造包含CancelToken的mock对象,一次性mock所有需要的Axios方法:

/**
 * @jest-environment jsdom
 */
import axios from 'axios';
import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';
import { useRouter } from 'next/router';

// Mock Next.js useRouter
jest.mock('next/router', () => ({
  useRouter: jest.fn(),
}));

// Mock 整个axios模块
jest.mock('axios', () => {
  const cancelTokenSource = { 
    cancel: jest.fn(), 
    token: { reason: { message: 'user canceled' } } 
  };
  return {
    CancelToken: {
      source: jest.fn(() => cancelTokenSource),
    },
    get: jest.fn().mockResolvedValueOnce('joe'),
  };
});

test('MyComponent renders fetched name correctly', async () => {
  // Mock router query参数
  useRouter.mockImplementationOnce(() => ({
    query: { cid: 'coffee' },
  }));

  render(<MyComponent />);
  
  // 等待异步请求完成后查找元素
  const titleElement = await screen.findByText('Hello joe');
  expect(titleElement).toBeInTheDocument();
  
  // 验证CancelToken.source方法被调用
  expect(axios.CancelToken.source).toHaveBeenCalled();
});

方案2:手动初始化CancelToken后再Spy

先给axios.CancelToken赋值一个包含source方法的对象,再使用jest.spyOn进行mock:

/**
 * @jest-environment jsdom
 */
import axios from 'axios';
import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';
import { useRouter } from 'next/router';

jest.mock('next/router', () => ({
  useRouter: jest.fn(),
}));

test('MyComponent', async () => {
  const cancelTokenSource: any = { 
    cancel: jest.fn(), 
    token: { reason: { message: 'user canceled' } } 
  };
  
  // 手动初始化axios.CancelToken
  axios.CancelToken = {
    source: jest.fn(() => cancelTokenSource),
  };
  
  // 对source方法进行spy
  jest.spyOn(axios.CancelToken, 'source').mockReturnValueOnce(cancelTokenSource);
  jest.spyOn(axios, 'get').mockResolvedValueOnce('joe');
  
  useRouter.mockImplementationOnce(() => ({
    query: { cid: 'coffee' },
  }));
  
  render(<MyComponent />);
       
  const titleElement = await screen.findByText('Hello joe');
  expect(titleElement).toBeInTheDocument();
});

额外验证:测试请求取消逻辑

可以添加测试用例验证组件卸载时是否正确调用了cancel方法:

test('MyComponent cancels request on unmount', async () => {
  const cancelTokenSource: any = { 
    cancel: jest.fn(), 
    token: { reason: { message: 'user canceled' } } 
  };
  
  axios.CancelToken = {
    source: jest.fn(() => cancelTokenSource),
  };
  
  useRouter.mockImplementationOnce(() => ({ query: { cid: 'coffee' } }));
  
  const { unmount } = render(<MyComponent />);
  // 卸载组件触发useEffect的清理函数
  unmount();
  
  // 验证cancel方法被调用,且参数正确
  expect(cancelTokenSource.cancel).toHaveBeenCalledWith('axios request cancelled');
});

内容的提问来源于stack exchange,提问作者nabil libre

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 23:09:34