如何用Jest和React Testing Library在useEffect中Mock axios.CancelToken
React测试中Mock Axios CancelToken的解决方案
问题背景
- 需求:通过React组件测试,该组件在
useEffect中使用Axios的CancelToken实现请求取消逻辑 - 尝试:使用
jest.spyOnmockaxios.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
相关产品推荐
相关产品推荐

